Skip to content

Commit 84f017c

Browse files
authored
feat:Add Together API Provider (RooCodeInc#1698)
1 parent 076b1e3 commit 84f017c

File tree

9 files changed

+185
-3
lines changed

9 files changed

+185
-3
lines changed

.changeset/green-oranges-sit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add Together API Provider

src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { OpenAiNativeHandler } from "./providers/openai-native"
1212
import { ApiStream } from "./transform/stream"
1313
import { DeepSeekHandler } from "./providers/deepseek"
1414
import { RequestyHandler } from "./providers/requesty"
15+
import { TogetherHandler } from "./providers/together"
1516
import { QwenHandler } from "./providers/qwen"
1617
import { MistralHandler } from "./providers/mistral"
1718
import { VsCodeLmHandler } from "./providers/vscode-lm"
@@ -51,6 +52,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
5152
return new DeepSeekHandler(options)
5253
case "requesty":
5354
return new RequestyHandler(options)
55+
case "together":
56+
return new TogetherHandler(options)
5457
case "qwen":
5558
return new QwenHandler(options)
5659
case "mistral":

src/api/providers/together.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
import OpenAI from "openai"
3+
import { withRetry } from "../retry"
4+
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
5+
import { ApiHandler } from "../index"
6+
import { convertToOpenAiMessages } from "../transform/openai-format"
7+
import { ApiStream } from "../transform/stream"
8+
import { convertToR1Format } from "../transform/r1-format"
9+
10+
export class TogetherHandler implements ApiHandler {
11+
private options: ApiHandlerOptions
12+
private client: OpenAI
13+
14+
constructor(options: ApiHandlerOptions) {
15+
this.options = options
16+
this.client = new OpenAI({
17+
baseURL: "https://api.together.xyz/v1",
18+
apiKey: this.options.togetherApiKey,
19+
})
20+
}
21+
22+
@withRetry()
23+
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
24+
const modelId = this.options.togetherModelId ?? ""
25+
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
26+
27+
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
28+
{ role: "system", content: systemPrompt },
29+
...convertToOpenAiMessages(messages),
30+
]
31+
32+
if (isDeepseekReasoner) {
33+
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
34+
}
35+
36+
const stream = await this.client.chat.completions.create({
37+
model: modelId,
38+
messages: openAiMessages,
39+
temperature: 0,
40+
stream: true,
41+
stream_options: { include_usage: true },
42+
})
43+
for await (const chunk of stream) {
44+
const delta = chunk.choices[0]?.delta
45+
if (delta?.content) {
46+
yield {
47+
type: "text",
48+
text: delta.content,
49+
}
50+
}
51+
52+
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
53+
yield {
54+
type: "reasoning",
55+
reasoning: (delta.reasoning_content as string | undefined) || "",
56+
}
57+
}
58+
59+
if (chunk.usage) {
60+
yield {
61+
type: "usage",
62+
inputTokens: chunk.usage.prompt_tokens || 0,
63+
outputTokens: chunk.usage.completion_tokens || 0,
64+
}
65+
}
66+
}
67+
}
68+
69+
getModel(): { id: string; info: ModelInfo } {
70+
return {
71+
id: this.options.togetherModelId ?? "",
72+
info: openAiModelInfoSaneDefaults,
73+
}
74+
}
75+
}

src/core/webview/ClineProvider.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type SecretKey =
4646
| "openAiNativeApiKey"
4747
| "deepSeekApiKey"
4848
| "requestyApiKey"
49+
| "togetherApiKey"
4950
| "qwenApiKey"
5051
| "mistralApiKey"
5152
| "authToken"
@@ -84,6 +85,7 @@ type GlobalStateKey =
8485
| "liteLlmModelId"
8586
| "qwenApiLine"
8687
| "requestyModelId"
88+
| "togetherModelId"
8789

8890
export const GlobalFileNames = {
8991
apiConversationHistory: "api_conversation_history.json",
@@ -450,6 +452,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
450452
deepSeekApiKey,
451453
requestyApiKey,
452454
requestyModelId,
455+
togetherApiKey,
456+
togetherModelId,
453457
qwenApiKey,
454458
mistralApiKey,
455459
azureApiVersion,
@@ -485,6 +489,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
485489
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
486490
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
487491
await this.storeSecret("requestyApiKey", requestyApiKey)
492+
await this.storeSecret("togetherApiKey", togetherApiKey)
488493
await this.storeSecret("qwenApiKey", qwenApiKey)
489494
await this.storeSecret("mistralApiKey", mistralApiKey)
490495
await this.updateGlobalState("azureApiVersion", azureApiVersion)
@@ -495,6 +500,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
495500
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
496501
await this.updateGlobalState("qwenApiLine", qwenApiLine)
497502
await this.updateGlobalState("requestyModelId", requestyModelId)
503+
await this.updateGlobalState("togetherModelId", togetherModelId)
498504
if (this.cline) {
499505
this.cline.api = buildApiHandler(message.apiConfiguration)
500506
}
@@ -1387,6 +1393,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
13871393
deepSeekApiKey,
13881394
requestyApiKey,
13891395
requestyModelId,
1396+
togetherApiKey,
1397+
togetherModelId,
13901398
qwenApiKey,
13911399
mistralApiKey,
13921400
azureApiVersion,
@@ -1434,6 +1442,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
14341442
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
14351443
this.getSecret("requestyApiKey") as Promise<string | undefined>,
14361444
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
1445+
this.getSecret("togetherApiKey") as Promise<string | undefined>,
1446+
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
14371447
this.getSecret("qwenApiKey") as Promise<string | undefined>,
14381448
this.getSecret("mistralApiKey") as Promise<string | undefined>,
14391449
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
@@ -1498,6 +1508,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
14981508
deepSeekApiKey,
14991509
requestyApiKey,
15001510
requestyModelId,
1511+
togetherApiKey,
1512+
togetherModelId,
15011513
qwenApiKey,
15021514
qwenApiLine,
15031515
mistralApiKey,
@@ -1596,6 +1608,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
15961608
"openAiNativeApiKey",
15971609
"deepSeekApiKey",
15981610
"requestyApiKey",
1611+
"togetherApiKey",
15991612
"qwenApiKey",
16001613
"mistralApiKey",
16011614
"authToken",

src/shared/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type ApiProvider =
99
| "gemini"
1010
| "openai-native"
1111
| "requesty"
12+
| "together"
1213
| "deepseek"
1314
| "qwen"
1415
| "mistral"
@@ -45,6 +46,8 @@ export interface ApiHandlerOptions {
4546
deepSeekApiKey?: string
4647
requestyApiKey?: string
4748
requestyModelId?: string
49+
togetherApiKey?: string
50+
togetherModelId?: string
4851
qwenApiKey?: string
4952
mistralApiKey?: string
5053
azureApiVersion?: string

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
188188
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
189189
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
190190
<VSCodeOption value="requesty">Requesty</VSCodeOption>
191+
<VSCodeOption value="together">Together</VSCodeOption>
191192
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
192193
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
193194
<VSCodeOption value="ollama">Ollama</VSCodeOption>
@@ -316,7 +317,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
316317

317318
{selectedProvider === "qwen" && (
318319
<div>
319-
<DropdownContainer className="dropdown-container" style={{position: "inherit"}}>
320+
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
320321
<label htmlFor="qwen-line-provider">
321322
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
322323
</label>
@@ -740,6 +741,37 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
740741
</div>
741742
)}
742743

744+
{selectedProvider === "together" && (
745+
<div>
746+
<VSCodeTextField
747+
value={apiConfiguration?.togetherApiKey || ""}
748+
style={{ width: "100%" }}
749+
type="password"
750+
onInput={handleInputChange("togetherApiKey")}
751+
placeholder="Enter API Key...">
752+
<span style={{ fontWeight: 500 }}>API Key</span>
753+
</VSCodeTextField>
754+
<VSCodeTextField
755+
value={apiConfiguration?.togetherModelId || ""}
756+
style={{ width: "100%" }}
757+
onInput={handleInputChange("togetherModelId")}
758+
placeholder={"Enter Model ID..."}>
759+
<span style={{ fontWeight: 500 }}>Model ID</span>
760+
</VSCodeTextField>
761+
<p
762+
style={{
763+
fontSize: "12px",
764+
marginTop: 3,
765+
color: "var(--vscode-descriptionForeground)",
766+
}}>
767+
<span style={{ color: "var(--vscode-errorForeground)" }}>
768+
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
769+
models. Less capable models may not work as expected.)
770+
</span>
771+
</p>
772+
</div>
773+
)}
774+
743775
{selectedProvider === "vscode-lm" && (
744776
<div>
745777
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">

webview-ui/src/components/settings/__tests__/APIOptions.spec.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen, fireEvent } from "@testing-library/react"
1+
import { render, screen } from "@testing-library/react"
22
import { describe, it, expect, vi } from "vitest"
33
import ApiOptions from "../ApiOptions"
44
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
@@ -23,7 +23,6 @@ vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
2323
describe("ApiOptions Component", () => {
2424
vi.clearAllMocks()
2525
const mockPostMessage = vi.fn()
26-
const mockSetApiConfiguration = vi.fn()
2726

2827
beforeEach(() => {
2928
global.vscode = { postMessage: mockPostMessage } as any
@@ -49,3 +48,49 @@ describe("ApiOptions Component", () => {
4948
expect(modelIdInput).toBeInTheDocument()
5049
})
5150
})
51+
52+
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
53+
const actual = await importOriginal()
54+
return {
55+
...actual,
56+
// your mocked methods
57+
useExtensionState: vi.fn(() => ({
58+
apiConfiguration: {
59+
apiProvider: "together",
60+
requestyApiKey: "",
61+
requestyModelId: "",
62+
},
63+
setApiConfiguration: vi.fn(),
64+
uriScheme: "vscode",
65+
})),
66+
}
67+
})
68+
69+
describe("ApiOptions Component", () => {
70+
vi.clearAllMocks()
71+
const mockPostMessage = vi.fn()
72+
73+
beforeEach(() => {
74+
global.vscode = { postMessage: mockPostMessage } as any
75+
})
76+
77+
it("renders Together API Key input", () => {
78+
render(
79+
<ExtensionStateContextProvider>
80+
<ApiOptions showModelOptions={true} />
81+
</ExtensionStateContextProvider>,
82+
)
83+
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
84+
expect(apiKeyInput).toBeInTheDocument()
85+
})
86+
87+
it("renders Together Model ID input", () => {
88+
render(
89+
<ExtensionStateContextProvider>
90+
<ApiOptions showModelOptions={true} />
91+
</ExtensionStateContextProvider>,
92+
)
93+
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
94+
expect(modelIdInput).toBeInTheDocument()
95+
})
96+
})

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const ExtensionStateContextProvider: React.FC<{
6868
config.openAiNativeApiKey,
6969
config.deepSeekApiKey,
7070
config.requestyApiKey,
71+
config.togetherApiKey,
7172
config.qwenApiKey,
7273
config.mistralApiKey,
7374
config.vsCodeLmModelSelector,

webview-ui/src/utils/validate.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
5858
return "You must provide a valid API key or choose a different provider."
5959
}
6060
break
61+
case "together":
62+
if (!apiConfiguration.togetherApiKey || !apiConfiguration.togetherModelId) {
63+
return "You must provide a valid API key or choose a different provider."
64+
}
65+
break
6166
case "ollama":
6267
if (!apiConfiguration.ollamaModelId) {
6368
return "You must provide a valid model ID."

0 commit comments

Comments
 (0)