Skip to content

feat(channels): auto-detect TTS models in channel test button#1826

Open
survivor998 wants to merge 2 commits into
looplj:unstablefrom
survivor998:fix/tts-channel-test-support
Open

feat(channels): auto-detect TTS models in channel test button#1826
survivor998 wants to merge 2 commits into
looplj:unstablefrom
survivor998:fix/tts-channel-test-support

Conversation

@survivor998

Copy link
Copy Markdown

Summary

  • The channel test button in AxonHub sends a hardcoded chat completion request that is incompatible with TTS models (e.g. Xiaomi MiMo TTS). This PR fixes the issue by auto-detecting TTS models and constructing the appropriate request format.
  • Added Audio field to llm.Request (previously a TODO) and openai.Request, enabling audio parameters to flow through the request pipeline.
  • The TestChannelOrchestrator now checks the model card's modalities.output to detect TTS models. For TTS models, it sends a request with role: assistant, modalities: ["text", "audio"], and an empty audio param (letting the provider use its default voice). For non-TTS models, the original chat completion request is preserved.

Changes

File Change
llm/model.go Added ChatCompletionAudioParam struct and Audio field to Request
llm/transformer/openai/model.go Added ChatCompletionAudioParam struct and Audio field to Request
llm/transformer/openai/outbound_convert.go Propagate Audio field in RequestFromLLM
internal/server/orchestrator/tester.go Added modelService dependency, isTTSModel, buildTestRequest, extractMessageText helpers; updated stream handling for audio responses
internal/server/gql/resolver.go Pass modelService to NewTestChannelOrchestrator

Test plan

  • Build passes (go build ./...)
  • Test a non-TTS model via the channel test button — should behave as before
  • Test a TTS model (e.g. Xiaomi MiMo TTS) via the channel test button — should return success
  • Test TTS model with streaming enabled — should accumulate audio transcript from stream deltas

🤖 Generated with Claude Code

survivor998 and others added 2 commits June 13, 2026 12:01
The channel test button previously sent hardcoded chat completion requests
that failed for TTS models (e.g. Xiaomi MiMo-TTS). This fix detects TTS
models via their model card's modalities.output field and automatically
builds the correct audio request format.

Changes:
- Add ChatCompletionAudioParam to llm.Request (was previously a TODO)
- Add Audio field to openai.Request and carry it in RequestFromLLM
- Add isTTSModel detection via model card modalities.output lookup
- Add buildTestRequest that constructs TTS-shaped requests for audio models
- Update response handling to extract audio transcripts from TTS responses
- Update streaming handler to accumulate audio transcripts from delta

Verified locally: mimo-v2.5-tts tests successfully on Xiaomi channels.
Non-TTS models continue to work as before (backward compatible).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the channel test button for TTS models by auto-detecting audio-output models via their model card and constructing a TTS-specific request. It also adds the Audio field to llm.Request / openai.Request, propagates it through the outbound pipeline, introduces a shared SanitizeUserID helper, and applies consistent user-ID sanitisation across Anthropic, Doubao, and Zai transformers.

  • TTS auto-detection: isTTSModel queries the model card's modalities.output; buildTestRequest branches on the result and is reused by both TestChannel and testSingleKey.
  • Audio field wiring: ChatCompletionAudioParam is added to both the internal llm.Request and openai.Request; the outbound converter propagates it with a nil guard.
  • User-ID cleanup: a new shared.SanitizeUserID enforces ^[a-zA-Z0-9_-]{6,128}$ and is applied in Anthropic, Doubao, and Zai transformers, which also gain a fallback from Metadata["user_id"] to the top-level User field.

Confidence Score: 3/5

The TTS test request is built with assumptions that hold for Xiaomi MiMo TTS but will actively fail against any standard OpenAI-compatible audio provider.

The two central issues are in buildTestRequest: the sole message carries role: "assistant" (most providers require conversations to start with a user turn) and the audio param is entirely empty (OpenAI-compatible providers treat voice as required). Any TTS channel that is not Xiaomi MiMo TTS will fail the test even when the channel is correctly configured. The rest of the change — field propagation, user-ID sanitisation, stream handling — looks correct.

internal/server/orchestrator/tester.go — specifically the buildTestRequest TTS branch

Important Files Changed

Filename Overview
internal/server/orchestrator/tester.go Core change: adds isTTSModel/buildTestRequest/extractMessageText helpers for TTS-aware channel testing. The TTS request is built with role: "assistant" and an empty audio param, both of which will fail against standard OpenAI-compatible audio providers.
llm/transformer/shared/userid.go New shared SanitizeUserID helper enforces ^[a-zA-Z0-9_-]{6,128}$ constraints; the inner padding branch is dead code but causes no runtime bug.
llm/model.go Adds ChatCompletionAudioParam struct and Audio *ChatCompletionAudioParam field to Request, following the existing omitzero convention for pointer fields.
llm/transformer/openai/outbound_convert.go Propagates Audio from the internal llm.Request to the outbound openai.Request with a nil guard; clean and correct.
internal/server/gql/resolver.go One-line wiring change to pass modelService to NewTestChannelOrchestrator; straightforward.

Reviews (1): Last reviewed commit: "feat(channels): auto-detect TTS models i..." | Re-trigger Greptile

Comment on lines +114 to +129
return &llm.Request{
Model: testModel,
Messages: []llm.Message{
{
Role: "assistant",
Content: llm.MessageContent{
Content: lo.ToPtr("Hello, this is a TTS test. Please respond with audio."),
},
},
},
Modalities: []string{"text", "audio"},
Audio: &llm.ChatCompletionAudioParam{},
MaxCompletionTokens: lo.ToPtr(int64(256)),
Stream: lo.ToPtr(useStream),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 TTS test message uses role: "assistant" instead of "user"

Standard OpenAI-compatible audio/TTS APIs (and most OpenAI-compatible providers) reject conversations whose first (and only) message carries role: "assistant" — the model expects input to come from a user turn and then produce an audio response. Sending an assistant-role message as the sole input will cause these providers to return an error (e.g., "messages[0].role must be 'user' or 'system'"), meaning any TTS channel other than Xiaomi MiMo TTS will always fail the channel test even when correctly configured.

Comment on lines +124 to +127
Modalities: []string{"text", "audio"},
Audio: &llm.ChatCompletionAudioParam{},
MaxCompletionTokens: lo.ToPtr(int64(256)),
Stream: lo.ToPtr(useStream),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Empty audio param will fail for providers that require voice

&llm.ChatCompletionAudioParam{} serialises to "audio": {} with both voice and format omitted. OpenAI's audio API (and any compatible provider) treats voice as a required field and will return an error when it is absent. This means the TTS channel test would fail for any provider that follows the OpenAI spec, even though the channel is otherwise correctly configured.

Consider defaulting to a reasonable voice (e.g. "alloy") and format (e.g. "mp3") so the test works against standard providers.

Comment on lines +22 to +27

if len(sanitized) < 6 {
sanitized = "user_" + sanitized
if len(sanitized) < 6 {
sanitized = sanitized + strings.Repeat("_", 6-len(sanitized))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inner padding branch is dead code

"user_" is 5 bytes. Prepending it to any non-empty sanitized string always produces a result of length ≥ 6, so the second if len(sanitized) < 6 check can never be true at runtime (the function already returns early on empty input, and the loop always writes at least one byte per input rune).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant