diff --git a/bun.lock b/bun.lock index b4571f1314a0..5fa9fe7bc539 100644 --- a/bun.lock +++ b/bun.lock @@ -624,6 +624,7 @@ "@zip.js/zip.js": "2.7.62", "ai": "catalog:", "ai-gateway-provider": "3.1.2", + "ai-sdk-devin": "0.3.6", "bonjour-service": "1.3.0", "chokidar": "4.0.3", "cross-spawn": "catalog:", @@ -3069,6 +3070,8 @@ "ai-gateway-provider": ["ai-gateway-provider@3.1.2", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="], + "ai-sdk-devin": ["ai-sdk-devin@0.3.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0" } }, "sha512-1yylOVYCfJ73tpUeSeOWlQdPCTlbdxMntF09jpVf1gHZTDoGZD+Bo49ireK7LFqAdxStGinIZlX2zx6EQgnxNw=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -6183,6 +6186,8 @@ "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Y6j3yivgoEUf/kutD/k5GX/mzZfioRFoSx0gbQ+mIOzMaH/vJv1rCkztiuvlLw5xRYQil7oxHUZvmSfXqOx1NQ=="], + "ai-sdk-devin/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], + "ajv-keywords/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 293a678e9bb5..8f789fdb0668 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -61,6 +61,7 @@ "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cerebras": "2.0.60", + "ai-sdk-devin": "0.3.6", "@ai-sdk/cohere": "3.0.27", "@ai-sdk/deepinfra": "2.0.41", "@ai-sdk/gateway": "3.0.104", diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index a0ab5eb3474d..808b80748297 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -131,6 +131,7 @@ const BUNDLED_PROVIDERS: Record Promise<(opts: any) => BundledSDK> "@ai-sdk/github-copilot": () => import("@opencode-ai/core/github-copilot/copilot-provider").then((m) => m.createOpenaiCompatible), "venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice), + "ai-sdk-devin": () => import("ai-sdk-devin").then((m) => m.createDevin), } type CustomModelLoader = (sdk: any, modelID: string, options?: Record, model?: Model) => Promise @@ -859,6 +860,70 @@ function custom(dep: CustomDep): Record { }, }, }), + devin: Effect.fnUntraced(function* (input: Info) { + const env = yield* dep.env() + const auth = yield* dep.auth(input.id) + const apiKey = env["DEVIN_API_KEY"] || (auth?.type === "api" ? auth.key : undefined) + + if (!apiKey) { + return { + autoload: false, + async getModel() { + throw new Error("DEVIN_API_KEY is missing. Set it with: export DEVIN_API_KEY=") + }, + } + } + + return { + autoload: !input.env.length || input.env.some((item) => env[item]), + options: { + apiKey, + }, + async discoverModels(): Promise> { + try { + // ponytail: ai-sdk-devin may not be installed at discovery time; catch handles it + const { createDevin } = await import("ai-sdk-devin") + const devin = createDevin({ apiKey }) + const modelList = (await devin.models()) as any[] + const models: Record = {} + + for (const m of modelList) { + const modelId = m.modelId || m.id + if (!modelId) continue + if (input.models[modelId]) continue + + models[modelId] = { + id: ModelV2.ID.make(modelId), + providerID: ProviderV2.ID.make("devin"), + name: m.name || modelId, + family: "", + api: { id: modelId, url: "", npm: "ai-sdk-devin" }, + status: "active", + headers: {}, + options: {}, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 128_000, output: 8_192 }, + capabilities: { + temperature: false, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: false, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + release_date: "", + variants: {}, + } + } + + return models + } catch { + return {} + } + }, + } + }), "snowflake-cortex": Effect.fnUntraced(function* (input: Info) { const env = yield* dep.env() const auth = yield* dep.auth(input.id) @@ -1516,6 +1581,19 @@ const layer = Layer.effect( // load env const envs = yield* env.all() + + // Pre-populate Devin in database for env auto-activation when DEVIN_API_KEY is set + if (envs["DEVIN_API_KEY"] && !database["devin"]) { + database["devin"] = { + id: ProviderV2.ID.make("devin"), + name: "Devin (ai)", + env: ["DEVIN_API_KEY"], + source: "custom", + models: {}, + options: {}, + } + } + for (const [id, provider] of Object.entries(database)) { const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue @@ -1603,6 +1681,20 @@ const layer = Layer.effect( }) } + const devin = ProviderV2.ID.make("devin") + if (discoveryLoaders[devin] && providers[devin] && isProviderAllowed(devin)) { + yield* Effect.promise(async () => { + try { + const discovered = await discoveryLoaders[devin]() + for (const [modelID, model] of Object.entries(discovered)) { + if (!providers[devin].models[modelID]) { + providers[devin].models[modelID] = model + } + } + } catch (e) {} + }) + } + for (const [id, provider] of Object.entries(providers)) { const providerID = ProviderV2.ID.make(id) if (!isProviderAllowed(providerID)) {