feat:Add Together API Provider (#1698)

This commit is contained in:
brownrw8 2025-02-07 11:55:48 -10:00 committed by GitHub
parent 076b1e39e6
commit 84f017c98e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 185 additions and 3 deletions

View file

@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add Together API Provider

View file

@ -12,6 +12,7 @@ import { OpenAiNativeHandler } from "./providers/openai-native"
import { ApiStream } from "./transform/stream"
import { DeepSeekHandler } from "./providers/deepseek"
import { RequestyHandler } from "./providers/requesty"
import { TogetherHandler } from "./providers/together"
import { QwenHandler } from "./providers/qwen"
import { MistralHandler } from "./providers/mistral"
import { VsCodeLmHandler } from "./providers/vscode-lm"
@ -51,6 +52,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
return new DeepSeekHandler(options)
case "requesty":
return new RequestyHandler(options)
case "together":
return new TogetherHandler(options)
case "qwen":
return new QwenHandler(options)
case "mistral":

View file

@ -0,0 +1,75 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { withRetry } from "../retry"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandler } from "../index"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { convertToR1Format } from "../transform/r1-format"
export class TogetherHandler implements ApiHandler {
private options: ApiHandlerOptions
private client: OpenAI
constructor(options: ApiHandlerOptions) {
this.options = options
this.client = new OpenAI({
baseURL: "https://api.together.xyz/v1",
apiKey: this.options.togetherApiKey,
})
}
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const modelId = this.options.togetherModelId ?? ""
const isDeepseekReasoner = modelId.includes("deepseek-reasoner")
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
]
if (isDeepseekReasoner) {
openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages])
}
const stream = await this.client.chat.completions.create({
model: modelId,
messages: openAiMessages,
temperature: 0,
stream: true,
stream_options: { include_usage: true },
})
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
reasoning: (delta.reasoning_content as string | undefined) || "",
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
getModel(): { id: string; info: ModelInfo } {
return {
id: this.options.togetherModelId ?? "",
info: openAiModelInfoSaneDefaults,
}
}
}

View file

@ -46,6 +46,7 @@ type SecretKey =
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "requestyApiKey"
| "togetherApiKey"
| "qwenApiKey"
| "mistralApiKey"
| "authToken"
@ -84,6 +85,7 @@ type GlobalStateKey =
| "liteLlmModelId"
| "qwenApiLine"
| "requestyModelId"
| "togetherModelId"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
@ -450,6 +452,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
@ -485,6 +489,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey)
await this.storeSecret("deepSeekApiKey", deepSeekApiKey)
await this.storeSecret("requestyApiKey", requestyApiKey)
await this.storeSecret("togetherApiKey", togetherApiKey)
await this.storeSecret("qwenApiKey", qwenApiKey)
await this.storeSecret("mistralApiKey", mistralApiKey)
await this.updateGlobalState("azureApiVersion", azureApiVersion)
@ -495,6 +500,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("liteLlmModelId", liteLlmModelId)
await this.updateGlobalState("qwenApiLine", qwenApiLine)
await this.updateGlobalState("requestyModelId", requestyModelId)
await this.updateGlobalState("togetherModelId", togetherModelId)
if (this.cline) {
this.cline.api = buildApiHandler(message.apiConfiguration)
}
@ -1387,6 +1393,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
mistralApiKey,
azureApiVersion,
@ -1434,6 +1442,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
this.getSecret("requestyApiKey") as Promise<string | undefined>,
this.getGlobalState("requestyModelId") as Promise<string | undefined>,
this.getSecret("togetherApiKey") as Promise<string | undefined>,
this.getGlobalState("togetherModelId") as Promise<string | undefined>,
this.getSecret("qwenApiKey") as Promise<string | undefined>,
this.getSecret("mistralApiKey") as Promise<string | undefined>,
this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
@ -1498,6 +1508,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
deepSeekApiKey,
requestyApiKey,
requestyModelId,
togetherApiKey,
togetherModelId,
qwenApiKey,
qwenApiLine,
mistralApiKey,
@ -1596,6 +1608,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
"openAiNativeApiKey",
"deepSeekApiKey",
"requestyApiKey",
"togetherApiKey",
"qwenApiKey",
"mistralApiKey",
"authToken",

View file

@ -9,6 +9,7 @@ export type ApiProvider =
| "gemini"
| "openai-native"
| "requesty"
| "together"
| "deepseek"
| "qwen"
| "mistral"
@ -45,6 +46,8 @@ export interface ApiHandlerOptions {
deepSeekApiKey?: string
requestyApiKey?: string
requestyModelId?: string
togetherApiKey?: string
togetherModelId?: string
qwenApiKey?: string
mistralApiKey?: string
azureApiVersion?: string

View file

@ -188,6 +188,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
<VSCodeOption value="openai-native">OpenAI</VSCodeOption>
<VSCodeOption value="openai">OpenAI Compatible</VSCodeOption>
<VSCodeOption value="requesty">Requesty</VSCodeOption>
<VSCodeOption value="together">Together</VSCodeOption>
<VSCodeOption value="vscode-lm">VS Code LM API</VSCodeOption>
<VSCodeOption value="lmstudio">LM Studio</VSCodeOption>
<VSCodeOption value="ollama">Ollama</VSCodeOption>
@ -316,7 +317,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
{selectedProvider === "qwen" && (
<div>
<DropdownContainer className="dropdown-container" style={{position: "inherit"}}>
<DropdownContainer className="dropdown-container" style={{ position: "inherit" }}>
<label htmlFor="qwen-line-provider">
<span style={{ fontWeight: 500, marginTop: 5 }}>Alibaba API Line</span>
</label>
@ -740,6 +741,37 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
</div>
)}
{selectedProvider === "together" && (
<div>
<VSCodeTextField
value={apiConfiguration?.togetherApiKey || ""}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("togetherApiKey")}
placeholder="Enter API Key...">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.togetherModelId || ""}
style={{ width: "100%" }}
onInput={handleInputChange("togetherModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
<span style={{ color: "var(--vscode-errorForeground)" }}>
(<span style={{ fontWeight: 500 }}>Note:</span> Cline uses complex prompts and works best with Claude
models. Less capable models may not work as expected.)
</span>
</p>
</div>
)}
{selectedProvider === "vscode-lm" && (
<div>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 2} className="dropdown-container">

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import ApiOptions from "../ApiOptions"
import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
@ -23,7 +23,6 @@ vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
describe("ApiOptions Component", () => {
vi.clearAllMocks()
const mockPostMessage = vi.fn()
const mockSetApiConfiguration = vi.fn()
beforeEach(() => {
global.vscode = { postMessage: mockPostMessage } as any
@ -49,3 +48,49 @@ describe("ApiOptions Component", () => {
expect(modelIdInput).toBeInTheDocument()
})
})
vi.mock("../../../context/ExtensionStateContext", async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
useExtensionState: vi.fn(() => ({
apiConfiguration: {
apiProvider: "together",
requestyApiKey: "",
requestyModelId: "",
},
setApiConfiguration: vi.fn(),
uriScheme: "vscode",
})),
}
})
describe("ApiOptions Component", () => {
vi.clearAllMocks()
const mockPostMessage = vi.fn()
beforeEach(() => {
global.vscode = { postMessage: mockPostMessage } as any
})
it("renders Together API Key input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const apiKeyInput = screen.getByPlaceholderText("Enter API Key...")
expect(apiKeyInput).toBeInTheDocument()
})
it("renders Together Model ID input", () => {
render(
<ExtensionStateContextProvider>
<ApiOptions showModelOptions={true} />
</ExtensionStateContextProvider>,
)
const modelIdInput = screen.getByPlaceholderText("Enter Model ID...")
expect(modelIdInput).toBeInTheDocument()
})
})

View file

@ -68,6 +68,7 @@ export const ExtensionStateContextProvider: React.FC<{
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,

View file

@ -58,6 +58,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
return "You must provide a valid API key or choose a different provider."
}
break
case "together":
if (!apiConfiguration.togetherApiKey || !apiConfiguration.togetherModelId) {
return "You must provide a valid API key or choose a different provider."
}
break
case "ollama":
if (!apiConfiguration.ollamaModelId) {
return "You must provide a valid model ID."