mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add Codex CLI (native) provider with local authentication
- Add codex-cli-native provider type to types package - Create CodexCliHandler for CLI authentication operations - Add CodexCliNative UI component with sign-in/sign-out functionality - Implement message handlers for authentication flow - Add secret storage for bearer token - Reuse OpenAI Native handler with locally obtained token - Keep UI text generic as requested
This commit is contained in:
parent
2263d86a20
commit
3f4b427062
12 changed files with 408 additions and 4 deletions
|
|
@ -181,6 +181,7 @@ export const SECRET_STATE_KEYS = [
|
|||
"geminiApiKey",
|
||||
"openAiNativeApiKey",
|
||||
"cerebrasApiKey",
|
||||
"codexCliOpenAiNativeToken",
|
||||
"deepSeekApiKey",
|
||||
"doubaoApiKey",
|
||||
"moonshotApiKey",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import {
|
|||
export const providerNames = [
|
||||
"anthropic",
|
||||
"claude-code",
|
||||
"codex-cli-native",
|
||||
"glama",
|
||||
"openrouter",
|
||||
"bedrock",
|
||||
|
|
@ -343,6 +344,11 @@ const vercelAiGatewaySchema = baseProviderSettingsSchema.extend({
|
|||
vercelAiGatewayModelId: z.string().optional(),
|
||||
})
|
||||
|
||||
const codexCliNativeSchema = apiModelIdProviderModelSchema.extend({
|
||||
codexCliPath: z.string().optional(),
|
||||
// No API key field - uses token from secrets
|
||||
})
|
||||
|
||||
const defaultSchema = z.object({
|
||||
apiProvider: z.undefined(),
|
||||
})
|
||||
|
|
@ -350,6 +356,7 @@ const defaultSchema = z.object({
|
|||
export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
|
||||
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
|
||||
claudeCodeSchema.merge(z.object({ apiProvider: z.literal("claude-code") })),
|
||||
codexCliNativeSchema.merge(z.object({ apiProvider: z.literal("codex-cli-native") })),
|
||||
glamaSchema.merge(z.object({ apiProvider: z.literal("glama") })),
|
||||
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
|
||||
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
|
||||
|
|
@ -391,6 +398,7 @@ export const providerSettingsSchema = z.object({
|
|||
apiProvider: providerNamesSchema.optional(),
|
||||
...anthropicSchema.shape,
|
||||
...claudeCodeSchema.shape,
|
||||
...codexCliNativeSchema.shape,
|
||||
...glamaSchema.shape,
|
||||
...openRouterSchema.shape,
|
||||
...bedrockSchema.shape,
|
||||
|
|
@ -483,7 +491,10 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str
|
|||
}
|
||||
|
||||
export const MODELS_BY_PROVIDER: Record<
|
||||
Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama">,
|
||||
Exclude<
|
||||
ProviderName,
|
||||
"fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama" | "codex-cli-native"
|
||||
>,
|
||||
{ id: ProviderName; label: string; models: string[] }
|
||||
> = {
|
||||
anthropic: {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,16 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
|||
return new AnthropicHandler(options)
|
||||
case "claude-code":
|
||||
return new ClaudeCodeHandler(options)
|
||||
case "codex-cli-native":
|
||||
// Reuse OpenAI Native handler with token from secrets
|
||||
// The token will be injected from the secret storage
|
||||
// Note: The token is stored in secrets as codexCliOpenAiNativeToken
|
||||
// and will be injected into openAiNativeApiKey for the handler
|
||||
return new OpenAiNativeHandler({
|
||||
...options,
|
||||
openAiNativeApiKey: (options as any).codexCliOpenAiNativeToken,
|
||||
openAiNativeBaseUrl: options.openAiNativeBaseUrl || "https://api.openai.com",
|
||||
})
|
||||
case "glama":
|
||||
return new GlamaHandler(options)
|
||||
case "openrouter":
|
||||
|
|
@ -166,7 +176,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
|
|||
case "vercel-ai-gateway":
|
||||
return new VercelAiGatewayHandler(options)
|
||||
default:
|
||||
apiProvider satisfies "gemini-cli" | undefined
|
||||
apiProvider satisfies "gemini-cli" | "codex-cli-native" | undefined
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,13 +267,37 @@ export class ContextProxy {
|
|||
const values = this.getValues()
|
||||
|
||||
try {
|
||||
return providerSettingsSchema.parse(values)
|
||||
const settings = providerSettingsSchema.parse(values)
|
||||
|
||||
// For codex-cli-native provider, inject the token from secrets
|
||||
if (settings.apiProvider === "codex-cli-native") {
|
||||
const token = this.getSecret("codexCliOpenAiNativeToken" as SecretStateKey)
|
||||
if (token) {
|
||||
// Add the token to the settings object so it can be used by the API handler
|
||||
;(settings as any).codexCliOpenAiNativeToken = token
|
||||
}
|
||||
}
|
||||
|
||||
return settings
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
TelemetryService.instance.captureSchemaValidationError({ schemaName: "ProviderSettings", error })
|
||||
}
|
||||
|
||||
return PROVIDER_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as ProviderSettings)
|
||||
const settings = PROVIDER_SETTINGS_KEYS.reduce(
|
||||
(acc, key) => ({ ...acc, [key]: values[key] }),
|
||||
{} as ProviderSettings,
|
||||
)
|
||||
|
||||
// For codex-cli-native provider, inject the token from secrets (fallback case)
|
||||
if (settings.apiProvider === "codex-cli-native") {
|
||||
const token = this.getSecret("codexCliOpenAiNativeToken" as SecretStateKey)
|
||||
if (token) {
|
||||
;(settings as any).codexCliOpenAiNativeToken = token
|
||||
}
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2323,6 +2323,102 @@ export const webviewMessageHandler = async (
|
|||
|
||||
break
|
||||
}
|
||||
case "codexCliNativeCheckToken": {
|
||||
// Check if token exists in secrets
|
||||
const token = await provider.context.secrets.get("codexCliOpenAiNativeToken")
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeTokenStatus",
|
||||
hasToken: !!token,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "codexCliNativeSignIn": {
|
||||
try {
|
||||
// Import the CLI handler module
|
||||
const { CodexCliHandler } = await import("../../services/codex-cli/CodexCliHandler")
|
||||
|
||||
// Get the CLI path from settings or use default
|
||||
const cliPath = message.text || "codex"
|
||||
|
||||
// Run the sign-in flow
|
||||
const handler = new CodexCliHandler(cliPath)
|
||||
const token = await handler.signIn()
|
||||
|
||||
if (token) {
|
||||
// Store the token in secrets
|
||||
await provider.context.secrets.store("codexCliOpenAiNativeToken", token)
|
||||
|
||||
// Notify the webview of success
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeSignInResult",
|
||||
success: true,
|
||||
})
|
||||
|
||||
// Update the state to reflect the new token
|
||||
await provider.postStateToWebview()
|
||||
} else {
|
||||
throw new Error("Failed to obtain token from CLI")
|
||||
}
|
||||
} catch (error) {
|
||||
provider.log(`CodexCliNative sign-in failed: ${error}`)
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeSignInResult",
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "codexCliNativeSignOut": {
|
||||
try {
|
||||
// Clear the token from secrets
|
||||
await provider.context.secrets.delete("codexCliOpenAiNativeToken")
|
||||
|
||||
// Notify the webview of success
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeSignOutResult",
|
||||
success: true,
|
||||
})
|
||||
|
||||
// Update the state
|
||||
await provider.postStateToWebview()
|
||||
} catch (error) {
|
||||
provider.log(`CodexCliNative sign-out failed: ${error}`)
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeSignOutResult",
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "codexCliNativeDetect": {
|
||||
try {
|
||||
// Import the CLI handler module
|
||||
const { CodexCliHandler } = await import("../../services/codex-cli/CodexCliHandler")
|
||||
|
||||
// Get the CLI path from settings or use default
|
||||
const cliPath = message.text || "codex"
|
||||
|
||||
// Check if CLI is available
|
||||
const handler = new CodexCliHandler(cliPath)
|
||||
const isAvailable = await handler.detect()
|
||||
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeDetectResult",
|
||||
available: isAvailable,
|
||||
path: isAvailable ? cliPath : undefined,
|
||||
})
|
||||
} catch (error) {
|
||||
provider.log(`CodexCliNative detect failed: ${error}`)
|
||||
await provider.postMessageToWebview({
|
||||
type: "codexCliNativeDetectResult",
|
||||
available: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "rooCloudManualUrl": {
|
||||
try {
|
||||
if (!message.text) {
|
||||
|
|
|
|||
111
src/services/codex-cli/CodexCliHandler.ts
Normal file
111
src/services/codex-cli/CodexCliHandler.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { spawn } from "child_process"
|
||||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Handler for Codex CLI authentication operations
|
||||
* Based on the ChatMock reference implementation
|
||||
*/
|
||||
export class CodexCliHandler {
|
||||
constructor(private cliPath: string = "codex") {}
|
||||
|
||||
/**
|
||||
* Detect if the CLI is available
|
||||
*/
|
||||
async detect(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const process = spawn(this.cliPath, ["--version"], {
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
})
|
||||
|
||||
process.on("error", () => {
|
||||
resolve(false)
|
||||
})
|
||||
|
||||
process.on("exit", (code) => {
|
||||
resolve(code === 0)
|
||||
})
|
||||
|
||||
// Timeout after 5 seconds
|
||||
setTimeout(() => {
|
||||
process.kill()
|
||||
resolve(false)
|
||||
}, 5000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the sign-in flow to obtain a bearer token
|
||||
*/
|
||||
async signIn(): Promise<string | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Run the CLI auth command
|
||||
const process = spawn(this.cliPath, ["auth", "login", "--json"], {
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
})
|
||||
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
process.stdout?.on("data", (data) => {
|
||||
stdout += data.toString()
|
||||
})
|
||||
|
||||
process.stderr?.on("data", (data) => {
|
||||
stderr += data.toString()
|
||||
})
|
||||
|
||||
process.on("error", (error) => {
|
||||
reject(new Error(`Failed to spawn CLI: ${error.message}`))
|
||||
})
|
||||
|
||||
process.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
// Parse the JSON output to extract the token
|
||||
const result = JSON.parse(stdout)
|
||||
if (result.token) {
|
||||
resolve(result.token)
|
||||
} else {
|
||||
reject(new Error("No token in CLI response"))
|
||||
}
|
||||
} catch (error) {
|
||||
// If JSON parsing fails, try to extract token from plain text
|
||||
const tokenMatch = stdout.match(/token[:\s]+([a-zA-Z0-9\-._~+/]+=*)/i)
|
||||
if (tokenMatch) {
|
||||
resolve(tokenMatch[1])
|
||||
} else {
|
||||
reject(new Error(`Failed to parse CLI output: ${stdout}`))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`CLI exited with code ${code}: ${stderr || stdout}`))
|
||||
}
|
||||
})
|
||||
|
||||
// Timeout after 2 minutes (to allow for browser auth flow)
|
||||
setTimeout(() => {
|
||||
process.kill()
|
||||
reject(new Error("Sign-in timed out"))
|
||||
}, 120000)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is valid by making a test API call
|
||||
*/
|
||||
async validateToken(token: string): Promise<boolean> {
|
||||
try {
|
||||
// Make a simple API call to validate the token
|
||||
const response = await fetch("https://api.openai.com/v1/models", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +124,10 @@ export interface ExtensionMessage {
|
|||
| "commands"
|
||||
| "insertTextIntoTextarea"
|
||||
| "dismissedUpsells"
|
||||
| "codexCliNativeTokenStatus"
|
||||
| "codexCliNativeSignInResult"
|
||||
| "codexCliNativeSignOutResult"
|
||||
| "codexCliNativeDetectResult"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -201,6 +205,9 @@ export interface ExtensionMessage {
|
|||
commands?: Command[]
|
||||
queuedMessages?: QueuedMessage[]
|
||||
list?: string[] // For dismissedUpsells
|
||||
hasToken?: boolean // For codexCliNativeTokenStatus
|
||||
available?: boolean // For codexCliNativeDetectResult
|
||||
path?: string // For codexCliNativeDetectResult
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -225,6 +225,10 @@ export interface WebviewMessage {
|
|||
| "editQueuedMessage"
|
||||
| "dismissUpsell"
|
||||
| "getDismissedUpsells"
|
||||
| "codexCliNativeCheckToken"
|
||||
| "codexCliNativeSignIn"
|
||||
| "codexCliNativeSignOut"
|
||||
| "codexCliNativeDetect"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import {
|
|||
Cerebras,
|
||||
Chutes,
|
||||
ClaudeCode,
|
||||
CodexCliNative,
|
||||
DeepSeek,
|
||||
Doubao,
|
||||
Gemini,
|
||||
|
|
@ -322,6 +323,7 @@ const ApiOptions = ({
|
|||
"claude-code": { field: "apiModelId", default: claudeCodeDefaultModelId },
|
||||
"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
|
||||
"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
|
||||
"codex-cli-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
|
||||
gemini: { field: "apiModelId", default: geminiDefaultModelId },
|
||||
deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
|
||||
doubao: { field: "apiModelId", default: doubaoDefaultModelId },
|
||||
|
|
@ -513,6 +515,13 @@ const ApiOptions = ({
|
|||
<ClaudeCode apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
|
||||
)}
|
||||
|
||||
{selectedProvider === "codex-cli-native" && (
|
||||
<CodexCliNative
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "openai-native" && (
|
||||
<OpenAI
|
||||
apiConfiguration={apiConfiguration}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import {
|
|||
export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
|
||||
anthropic: anthropicModels,
|
||||
"claude-code": claudeCodeModels,
|
||||
"codex-cli-native": openAiNativeModels, // Reuses OpenAI native models
|
||||
bedrock: bedrockModels,
|
||||
cerebras: cerebrasModels,
|
||||
deepseek: deepSeekModels,
|
||||
|
|
@ -51,6 +52,7 @@ export const PROVIDERS = [
|
|||
{ value: "deepinfra", label: "DeepInfra" },
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
{ value: "claude-code", label: "Claude Code" },
|
||||
{ value: "codex-cli-native", label: "Codex CLI (native)" },
|
||||
{ value: "cerebras", label: "Cerebras" },
|
||||
{ value: "gemini", label: "Google Gemini" },
|
||||
{ value: "doubao", label: "Doubao" },
|
||||
|
|
|
|||
128
webview-ui/src/components/settings/providers/CodexCliNative.tsx
Normal file
128
webview-ui/src/components/settings/providers/CodexCliNative.tsx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { useState, useEffect } from "react"
|
||||
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ProviderSettings } from "@roo-code/types"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type CodexCliNativeProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
}
|
||||
|
||||
export const CodexCliNative = ({ apiConfiguration, setApiConfigurationField }: CodexCliNativeProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [isSignedIn, setIsSignedIn] = useState(false)
|
||||
const [isSigningIn, setIsSigningIn] = useState(false)
|
||||
const [showCustomPath, setShowCustomPath] = useState(false)
|
||||
|
||||
// Check if user is signed in by checking if token exists
|
||||
useEffect(() => {
|
||||
// Request token status from extension
|
||||
vscode.postMessage({ type: "codexCliNativeCheckToken" })
|
||||
}, [])
|
||||
|
||||
// Listen for token status updates
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data
|
||||
if (message.type === "codexCliNativeTokenStatus") {
|
||||
setIsSignedIn(message.hasToken)
|
||||
} else if (message.type === "codexCliNativeSignInResult") {
|
||||
setIsSigningIn(false)
|
||||
if (message.success) {
|
||||
setIsSignedIn(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [])
|
||||
|
||||
const handleSignIn = () => {
|
||||
setIsSigningIn(true)
|
||||
vscode.postMessage({ type: "codexCliNativeSignIn" })
|
||||
}
|
||||
|
||||
const handleSignOut = () => {
|
||||
vscode.postMessage({ type: "codexCliNativeSignOut" })
|
||||
setIsSignedIn(false)
|
||||
}
|
||||
|
||||
const handleDetect = () => {
|
||||
vscode.postMessage({ type: "codexCliNativeDetect" })
|
||||
}
|
||||
|
||||
const handlePathChange = (event: Event | React.FormEvent<HTMLElement>) => {
|
||||
const value = inputEventTransform(event as Event)
|
||||
setApiConfigurationField("codexCliPath", value as string)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{isSignedIn ? (
|
||||
<>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.codexCliNative.signedInMessage")}
|
||||
</div>
|
||||
<VSCodeButton appearance="secondary" onClick={handleSignOut} className="w-fit">
|
||||
{t("settings:providers.codexCliNative.signOutButton")}
|
||||
</VSCodeButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.codexCliNative.signInMessage")}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
onClick={handleSignIn}
|
||||
disabled={isSigningIn}
|
||||
className="w-fit">
|
||||
{isSigningIn
|
||||
? t("settings:providers.codexCliNative.signingInButton")
|
||||
: t("settings:providers.codexCliNative.signInButton")}
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={handleDetect} className="w-fit">
|
||||
{t("settings:providers.codexCliNative.detectButton")}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCustomPath(!showCustomPath)}
|
||||
className="text-sm text-vscode-link hover:underline cursor-pointer">
|
||||
{showCustomPath
|
||||
? t("settings:providers.codexCliNative.hideCustomPath")
|
||||
: t("settings:providers.codexCliNative.showCustomPath")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showCustomPath && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.codexCliPath || ""}
|
||||
type="text"
|
||||
onInput={handlePathChange}
|
||||
placeholder={t("settings:providers.codexCliNative.pathPlaceholder")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.codexCliNative.pathLabel")}
|
||||
</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground mt-1">
|
||||
{t("settings:providers.codexCliNative.pathDescription")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ export { Bedrock } from "./Bedrock"
|
|||
export { Cerebras } from "./Cerebras"
|
||||
export { Chutes } from "./Chutes"
|
||||
export { ClaudeCode } from "./ClaudeCode"
|
||||
export { CodexCliNative } from "./CodexCliNative"
|
||||
export { DeepSeek } from "./DeepSeek"
|
||||
export { Doubao } from "./Doubao"
|
||||
export { Gemini } from "./Gemini"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue