feat(tts): add Inworld as built-in speech provider#55972
Conversation
Greptile SummaryThis PR adds Inworld AI as a fourth built-in TTS provider alongside OpenAI, ElevenLabs, and Microsoft. The integration follows the existing provider plugin pattern well: a new Key findings:
Confidence Score: 4/5Safe to merge after addressing the temperature schema bug and reverting the unrelated pull_policy: never docker-compose change. Two P1 findings remain: a schema validation bug that rejects a documented valid input value, and an unrelated docker-compose change that breaks image-pull deployments. The core implementation is otherwise solid and well-tested. src/config/zod-schema.core.ts (temperature range) and docker-compose.yml (pull_policy: never).
|
| Filename | Overview |
|---|---|
| extensions/inworld/speech-provider.ts | Core provider implementation — streaming NDJSON parser, voice listing, telephony support. Logic is correct; minor robustness gap with unguarded JSON.parse. |
| src/config/zod-schema.core.ts | Inworld Zod schema added; temperature validation uses gt(0) instead of gte(0), contradicting the documented 0–2 range. |
| docker-compose.yml | Adds INWORLD_API_KEY env passthrough (correct), but also adds pull_policy: never to both services — an unrelated change that breaks image-pull deployments. |
| src/tts/tts.ts | Inworld wired into resolveTtsConfig, resolveTtsApiKey, getTtsProvider auto-detection, and TTS_PROVIDERS — all consistent with existing provider patterns. |
| extensions/inworld/speech-provider.test.ts | 17 unit tests covering synthesis, voice listing, error handling, format selection, and provider metadata — good coverage. |
Comments Outside Diff (1)
-
docker-compose.yml, line 31 (link)pull_policy: neverbreaks image-pull deploymentsAdding
pull_policy: neverto bothopenclaw-gateway(line 31) andopenclaw-cli(line 48) means Docker will never attempt to pull a pre-built image from a registry. Any user who does not already have the image locally and runsdocker-compose upwill get an immediate"unable to find image ... locally"error instead of pulling the image.This appears to be a local-development convenience setting that leaked into the shared
docker-compose.yml. It is also unrelated to the Inworld TTS feature and should either be reverted or kept only in a local override file (docker-compose.override.yml).Prompt To Fix With AI
This is a comment left during a code review. Path: docker-compose.yml Line: 31 Comment: **`pull_policy: never` breaks image-pull deployments** Adding `pull_policy: never` to both `openclaw-gateway` (line 31) and `openclaw-cli` (line 48) means Docker will never attempt to pull a pre-built image from a registry. Any user who does not already have the image locally and runs `docker-compose up` will get an immediate `"unable to find image ... locally"` error instead of pulling the image. This appears to be a local-development convenience setting that leaked into the shared `docker-compose.yml`. It is also unrelated to the Inworld TTS feature and should either be reverted or kept only in a local override file (`docker-compose.override.yml`). How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/zod-schema.core.ts
Line: 455
Comment:
**Temperature validation rejects documented minimum value**
The Zod schema uses `.gt(0)` (strictly greater than 0), but the JSDoc comment in `src/config/types.tts.ts` defines the valid range as `"0–2"`, meaning `temperature: 0` is a documented and plausible value (e.g. for deterministic, low-variance synthesis). Any user who tries to set `temperature: 0` will get a schema validation error, silently contradicting the documented range.
```suggestion
temperature: z.number().gte(0).lte(2).optional(),
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: docker-compose.yml
Line: 31
Comment:
**`pull_policy: never` breaks image-pull deployments**
Adding `pull_policy: never` to both `openclaw-gateway` (line 31) and `openclaw-cli` (line 48) means Docker will never attempt to pull a pre-built image from a registry. Any user who does not already have the image locally and runs `docker-compose up` will get an immediate `"unable to find image ... locally"` error instead of pulling the image.
This appears to be a local-development convenience setting that leaked into the shared `docker-compose.yml`. It is also unrelated to the Inworld TTS feature and should either be reverted or kept only in a local override file (`docker-compose.override.yml`).
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/inworld/speech-provider.ts
Line: 88-94
Comment:
**Unguarded `JSON.parse` in streaming loop**
If the Inworld API returns a line that is not valid JSON (e.g., a rate-limit HTML error page that slips through after a `200` status, or a partial flush), `JSON.parse(trimmed)` will throw a raw `SyntaxError`. The error will propagate out of `inworldTTS`, be caught by the provider-level try/catch in `synthesizeSpeech`, and surface as a cryptic `"Unexpected token …"` message rather than something actionable.
Wrapping the parse in a try/catch with a descriptive re-throw would make debugging much easier:
```ts
let parsed: { result?: { audioContent?: string }; error?: { code?: number; message?: string } };
try {
parsed = JSON.parse(trimmed) as typeof parsed;
} catch {
throw new Error(`Inworld TTS stream parse error: unexpected non-JSON line: ${trimmed.slice(0, 80)}`);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(tts): add Inworld as built-in speec..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 56464e254c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
56464e2 to
7acb068
Compare
7acb068 to
67307c3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 67307c31d6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
c162c63 to
2fe6fa5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49374a424a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Deep review against current Current state:
Good changes since the earlier bot review:
Remaining blockers:
So: valid provider, not solved on |
f3e9c11 to
ff27a84
Compare
|
@steipete Thanks for the detailed review. I've pushed updates addressing all four blockers:
One incidental: while rebasing, I also realigned the SDK imports from |
ff27a84 to
f7b4ecd
Compare
f7b4ecd to
306e9c2
Compare
|
@obviyus I saw you merged in a Gemini TTS PR (#67515) recently. If you could take a look at this PR for Inworld TTS (currently #1 on Artificial Analysis TTS arena) that would be much appreciated! Thanks! |
306e9c2 to
9f7fcda
Compare
Registers a new bundled speech plugin at extensions/inworld/ that follows the openclaw/plugin-sdk/* contract: the plugin entry registers an Inworld SpeechProviderPlugin via api.registerSpeechProvider, and the manifest declares the speechProviders contract with enabledByDefault=true so core picks it up through normal plugin discovery. Provider covers MP3 output, OGG_OPUS voice-notes, PCM telephony, listVoices, and API-key resolution via messages.tts.providers.inworld.apiKey or INWORLD_API_KEY. Uses the shared openclaw/plugin-sdk/speech coerce helpers to stay consistent with the OpenAI and ElevenLabs bundled providers. Docs, .env.example, and the Unreleased changelog are updated; no core src/tts, src/config, or src/secrets edits are needed under the current plugin-owned pattern.
CI lint:tmp:no-raw-channel-fetch flags new raw fetch() callsites in extension runtime code. Replace the Inworld streaming-TTS and voices calls with fetchWithSsrFGuard from openclaw/plugin-sdk/ssrf-runtime, so SSRF policy, dispatcher pinning, and proxy-capture run through the shared guard instead of a plugin-local AbortController. Tests now mock the ssrf-runtime module export instead of globalThis.fetch and assert the guarded request shape + dispatcher release for both success and failure paths.
- Switch extensions/inworld imports from openclaw/plugin-sdk/speech to openclaw/plugin-sdk/speech-core to match the merged Gemini TTS template. - Enrich extensions/inworld/openclaw.plugin.json with name, description, providerAuthEnvVars (INWORLD_API_KEY), and a real configSchema so the control-plane can validate and surface field-level help. - Add the matching .github/labeler.yml entry for extensions: inworld. - Document the HTTP Basic / Base64-credential auth expectation in docs/tools/tts.md and inline near the Authorization header in tts.ts. - Add docs/providers/inworld.md provider page and wire it into the Providers nav in docs/docs.json. - Regenerate docs/.generated/config-baseline.sha256 for the manifest configSchema change.
Update DEFAULT_INWORLD_VOICE_ID, the corresponding default-voice test expectation, manifest configSchema description, and all docs (messages.tts.providers.inworld example, the inworld provider page, and the field reference table) to point at Sarah by default.
45c931b to
e2fbe6a
Compare
|
Landed in 0bcb4c9. Maintainer proof:
Also verified no open Inworld duplicate issues/PRs remain. |
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Adds the bundled Inworld speech provider with docs, config surface, SSRF-guarded fetches, directive overrides, native voice-note/telephony output coverage, and live `.profile` verification. Co-authored-by: cshape <[email protected]>
Summary
Add Inworld as a built-in OpenClaw speech provider plugin, alongside ElevenLabs, Google Gemini, Gradium, Local CLI, Microsoft, MiniMax, OpenAI, Vydra, xAI, and Xiaomi MiMo. Inworld is a streaming TTS API; this PR wires it into
messages.ttswith MP3, OGG_OPUS (voice notes), and PCM (telephony) outputs.New files
extensions/inworld/speech-provider.ts— speech-provider plugin entry, including audio format negotiation (MP3 / OGG_OPUS / PCM) and voice listing.extensions/inworld/tts.ts— Inworld API client: streamingvoice:stream(newline-delimited JSON, base64 audio chunks) andvoices/v1/voiceslisting. All HTTP requests routed throughfetchWithSsrFGuard.extensions/inworld/speech-provider.test.ts,extensions/inworld/tts.test.ts— 22 unit tests covering streaming chunk parsing, error paths, voice mapping, telephony PCM, and request body shape.extensions/inworld/index.ts— plugin entry point.extensions/inworld/package.json,extensions/inworld/tsconfig.json— workspace package metadata.extensions/inworld/openclaw.plugin.json— plugin manifest withname,description,providerAuthEnvVars: { inworld: ["INWORLD_API_KEY"] },contracts.speechProviders, and a realconfigSchemaforapiKey,baseUrl,voiceId,modelId, andtemperature(0..2 bounded).docs/providers/inworld.md— provider page modeled ondocs/providers/deepgram.md: setup, config table, models, audio outputs, custom endpoints, and the Base64 / HTTP Basic auth callout. Wired intodocs/docs.jsonProviders nav.Modified files
.env.example—INWORLD_API_KEYplaceholder..github/labeler.yml—extensions: inworldlabel rule coveringextensions/inworld/**anddocs/providers/inworld.md(the matching GitHub label needs to be created repo-side, color#0E8A16to match otherextensions: *labels).CHANGELOG.md— new Inworld TTS entry under the Unreleased ## Changes section.docs/tools/tts.md— Inworld added to provider list, optional-keys section, primary config example, and field reference; auth note explicitly states the apiKey must be the Base64 dashboard credential sent verbatim asAuthorization: Basic <apiKey>(no bearer-style normalization, no extra encoding).docs/.generated/config-baseline.sha256— regenerated for the manifest configSchema additions.Inworld API details
POST /tts/v1/voice:stream— newline-delimited JSON, each line carries base64 audio inresult.audioContent.GET /voices/v1/voices(current Voices API; old/tts/v1/voicesis deprecated).Authorization: Basic <apiKey>whereapiKeyis the Base64-encoded credential string copied verbatim from the Inworld dashboard. Plugin sends it without re-encoding; users should not pass a raw bearer token. Fallback resolution:messages.tts.providers.inworld.apiKey→INWORLD_API_KEYenv.Sarah. Default model:inworld-tts-1.5-max.inworld-tts-1.5-max,inworld-tts-1.5-mini,inworld-tts-1-max,inworld-tts-1.target: "voice-note"; raw PCM at 22050 Hz for telephony.apiKey,baseUrl,voiceId,modelId,temperature(0..2).Config example
{ "messages": { "tts": { "auto": "always", "provider": "inworld", "providers": { "inworld": { "voiceId": "Sarah", "modelId": "inworld-tts-1.5-max" } } } } }Set
INWORLD_API_KEYin env (Base64 credential from the Inworld dashboard, used as HTTP Basic).Notes for reviewers
openclaw/plugin-sdk/speech-coresubpath, matching the merged Gemini TTS template (feat(google): add Gemini TTS provider #67515).providerAuthChoices/uiHints— those are model-provider onboarding fields).Test plan
pnpm test extensions/inworld— 22/22 passpnpm test:bundled— pass (bundled plugin invariants)pnpm config:docs:check— pass after manifest configSchema regenpnpm build— passvoiceId: "Sarah"round-trips correctly