Skip to content

feat(desktop): add Together AI as a provider option - #3055

Open
jckail wants to merge 2 commits into
block:mainfrom
jckail:feat/together-ai-provider
Open

feat(desktop): add Together AI as a provider option#3055
jckail wants to merge 2 commits into
block:mainfrom
jckail:feat/together-ai-provider

Conversation

@jckail

@jckail jckail commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Adds Together AI to the provider picker in Agent defaults, agent templates, and per-agent config.

Together speaks stock OpenAI Chat Completions, so this needs no new wire dialect in buzz-agent. It follows the preset pattern relay-mesh ("Buzz shared compute") already established: the desktop stores together as the provider and derives BUZZ_AGENT_PROVIDER=openai plus Together's base URL at spawn, in a new managed_agents/together.rs. The Provider enum, Llm::complete, and the effort tables are untouched.

Three things follow from Together naming its own credential and shipping an unusually large catalog:

  • The credential stays TOGETHER_API_KEY everywhere the user can see it — picker, readiness gate, model discovery — and is mapped onto OPENAI_COMPAT_API_KEY only at spawn. Requiring the mapped name would ask for a key the dialog never offers. Both OpenAI keys are scrubbed before the mapped one is written back, so a key left in the env or inherited from the shell cannot reach Together's ingress.
  • /v1/models returns a bare array, not OpenAI's {"data": [...]} envelope, so OpenAiModelListResponse became an untagged enum accepting both shapes.
  • The catalog mixes ~2k endpoints across every modality (chat, image, audio, embedding, rerank, …), and Together's namespaced ids like moonshotai/Kimi-K2.6 defeat the id-shape heuristic used for OpenAI. Discovery filters on the declared type == "chat" and prefers Together's display_name for labels.

Together requires an explicit model — it serves well over a thousand chat models and picks no default — so the picker offers no "Default model" entry, same as Anthropic and OpenAI.

Three check-file-sizes.mjs ceilings are ratcheted (agent_models.rs +50, readiness.rs +74, runtime.rs +23), each with a comment explaining the growth. The preset logic itself lives in the new file; what lands in the existing ones is the spawn call site, the credential arms, and the discovery branch, which cannot move.

Related issue

None found. The closest existing work is #1975 (first-class OpenRouter support), which is deliberately a much heavier change — OpenRouter needs a real Provider enum variant for its body mutations, max_tokens spelling, and attribution headers. Together needs none of that, so this stays a preset. The two PRs touch overlapping files (agentConfigOptions.tsx, readiness.rs, agent_models.rs, check-file-sizes.mjs) and will likely need a trivial merge resolution, whichever lands second.

Testing

  • End-to-end against the live Together API. Drove the real buzz-agent binary over ACP stdio using exactly the env apply_together_env derives — initializesession/newsession/prompt completed with stopReason: end_turn on moonshotai/Kimi-K2.6. Separately confirmed tool calling works over that transport (finish_reason: tool_calls).
  • Model discovery verified against the live catalog: 2021 entries, 1743 of type chat, returned as a bare array — the two behaviors the parsing and filtering changes exist for.
  • 10 new Rust tests covering the env derivation (including that a missing key writes no credential, so a scrubbed one stays scrubbed), the readiness gate asking for TOGETHER_API_KEY and not the mapped OpenAI key, bare-array parsing, and chat-only filtering.
  • 4 new TS tests covering the dropdown entry, credential mapping for both provider-selection runtimes, labels, and the explicit-model requirement.
  • Full suites green: cargo test (1791 passed), pnpm test (3633 passed), pnpm typecheck, pnpm check, cargo fmt --check, cargo clippy clean. Builds verified both with and without the mesh-llm feature.

Together is scoped to buzz-agent. I originally left the Goose side as an open question, then checked: Goose ships no Together provider (it exists only in an unmerged fork PR, and there under the id togetherai), while buzz-persona pipes the provider id straight into GOOSE_PROVIDER. Offering it for Goose would therefore persist GOOSE_PROVIDER=together and fail at startup, so the second commit hides it for provider-selecting runtimes other than buzz-agent — the same treatment AgentConfigFields already gives relay-mesh. An agent already saved with Together still renders its value via the (current) tail. The credential requirement stays keyed on the provider for both runtimes, keeping the Rust/TS mirror intact.

Made with Cursor

Together speaks stock OpenAI Chat Completions, so it needs no new wire
dialect in buzz-agent — only the preset that relay-mesh already
established: store `together` as the provider and derive
BUZZ_AGENT_PROVIDER=openai plus Together's base URL at spawn.

The credential stays TOGETHER_API_KEY everywhere the user can see it
(picker, readiness gate, model discovery) and is mapped onto
OPENAI_COMPAT_API_KEY only at spawn, so nothing asks for a key the
dialog never offers. Both OpenAI keys are scrubbed before the mapped one
is written back, so an unrelated key in the env cannot reach Together.

Model discovery needed two accommodations: Together answers /v1/models
with a bare array rather than OpenAI's {"data": [...]} envelope, and its
catalog mixes ~2k endpoints across every modality, so only entries
declaring type "chat" are offered.

Signed-off-by: jordan <jkail@together.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
@jckail
jckail requested a review from a team as a code owner July 27, 2026 01:48
@jckail

jckail commented Jul 27, 2026

Copy link
Copy Markdown
Author

Verification update — I ran this against the live Together API with a real key, plus the full local CI suite.

Live, through the production code paths (not reimplementations):

  • Model discovery — drove the real discover_openai_compatible_models against Together's live catalog. Returned 482 chat models, correctly parsed from the bare array, with Together's display_name used for labels (zai-org/GLM-5.2 → "GLM 5.2") and zero non-chat modalities leaking into the picker.
  • Agent sessions with tool calls — spawned the real buzz-agent binary with the env produced by the real apply_together_env, attached buzz-dev-mcp, and ran full ACP sessions. Each one executed a shell tool call with an observable side effect on disk and finished end_turn. Verified on five models: moonshotai/Kimi-K2.6, zai-org/GLM-5.2, openai/gpt-oss-120b, Qwen/Qwen3.5-9B, deepseek-ai/DeepSeek-V4-Pro.
  • Failure paths — with no key the agent fails fast at startup (config: OPENAI_COMPAT_API_KEY required), which is exactly what the readiness gate exists to prevent reaching; with a wrong key, the credential never appears in agent output or error text.

UI — confirmed in the running app that "Together AI" lands in the Agent defaults dropdown between OpenAI-compatible and Buzz shared compute, and that selecting it asks for a "Together AI API Key" (required) rather than the OpenAI label, with Model required and discovery running.

Suitesjust ci passes: workspace fmt-check, clippy -D warnings, test-unit, desktop-check, desktop-tauri-fmt-check, desktop-tauri-clippy, web-check, desktop-test (3633), desktop-build, desktop-tauri-check, desktop-tauri-test (1791), web-build. Playwright smoke: 723 passed. The 41 provider-related e2e tests (agent-provider-dropdowns, global-agent-config-screenshots, edit-agent, persona-env-vars, agent-readiness-screenshots) all pass, so the extra dropdown entry doesn't disturb the screenshot baselines.

Three smoke failures showed up in the full parallel run and are not from this change — I checked each against main with a fresh build rather than assuming:

Spec On this branch On main Conclusion
video-attachment.spec.ts:223 5/5 fail 5/5 fail Pre-existing on main
animated-avatar.spec.ts:60 3/3 pass 3/3 pass Flaky under full-suite load
persistent-agent-audience.spec.ts:189 3/3 pass 3/3 pass Flaky under full-suite load

mobile-check / mobile-test did not run — the Flutter iOS artifact download exhausted disk on my machine. This change touches no files under mobile/.

No test scaffolding is included in the PR; the live harnesses above were temporary and the branch is unchanged since it was opened.

Only buzz-agent gets BUZZ_AGENT_PROVIDER rewritten to Together's
OpenAI-compatible transport at spawn. Nothing rewrites GOOSE_PROVIDER,
and goose ships no provider under this id, so a goose agent configured
with Together would persist GOOSE_PROVIDER=together and fail at startup.

Gate the option on the runtime inside getPersonaProviderOptions rather
than at each picker, so none of the four call sites has to remember the
exclusion. The (current) tail still renders, so an agent already saved
with Together keeps showing its value instead of appearing unset.

This mirrors the existing relay-mesh treatment in AgentConfigFields,
which hides Buzz shared compute for the same reason.

Signed-off-by: jordan <jkail@together.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants