From cb74cc26065cd5858827d3668b5848e4818b5459 Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:19:16 +0800 Subject: [PATCH 1/2] test(provider): cover internal aggregator endpoints --- docs/examples/internal-llm-aggregator.jsonc | 60 +++++++++++++++ .../opencode/test/provider/provider.test.ts | 76 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 docs/examples/internal-llm-aggregator.jsonc diff --git a/docs/examples/internal-llm-aggregator.jsonc b/docs/examples/internal-llm-aggregator.jsonc new file mode 100644 index 0000000000..e37503179d --- /dev/null +++ b/docs/examples/internal-llm-aggregator.jsonc @@ -0,0 +1,60 @@ +{ + // 聚合平台按其暴露的标准协议分别配置端点。 + // 这不是网络白名单;普通公网与封闭内网使用同一套 Provider 机制。 + "provider": { + "internal-openai": { + "name": "Internal OpenAI Protocol", + "npm": "@ai-sdk/openai", + "env": ["LLM_AGGREGATOR_API_KEY"], + "options": { + "baseURL": "https://llm-api.example.internal/openai/v1" + }, + "models": { + "gpt-5": { + "name": "GPT-5 via Internal Aggregator", + "tool_call": true, + "limit": { + "context": 400000, + "output": 128000 + } + } + } + }, + "internal-anthropic": { + "name": "Internal Anthropic Protocol", + "npm": "@ai-sdk/anthropic", + "env": ["LLM_AGGREGATOR_API_KEY"], + "options": { + "baseURL": "https://llm-api.example.internal/anthropic" + }, + "models": { + "claude-sonnet-4-20250514": { + "name": "Claude Sonnet 4 via Internal Aggregator", + "tool_call": true, + "limit": { + "context": 200000, + "output": 64000 + } + } + } + }, + "internal-gemini": { + "name": "Internal Gemini Protocol", + "npm": "@ai-sdk/google", + "env": ["LLM_AGGREGATOR_API_KEY"], + "options": { + "baseURL": "https://llm-api.example.internal/gemini/v1beta" + }, + "models": { + "gemini-2.5-pro": { + "name": "Gemini 2.5 Pro via Internal Aggregator", + "tool_call": true, + "limit": { + "context": 1048576, + "output": 65536 + } + } + } + } + } +} diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index e2eb4dc89a..84ae9fddee 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -3,6 +3,7 @@ import { mkdir, unlink } from "fs/promises" import path from "path" import { Effect, Layer } from "effect" import { ModelsDev } from "@opencode-ai/core/models-dev" +import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { FSUtil } from "@opencode-ai/core/fs-util" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Global } from "@opencode-ai/core/global" @@ -16,6 +17,7 @@ import { import { markPluginDependenciesReady } from "../fixture/plugin" import { Auth } from "@/auth" import { Config } from "@/config/config" +import { ConfigParse } from "@/config/parse" import { Env } from "../../src/env" import { Plugin } from "../../src/plugin/index" import { Provider } from "@/provider/provider" @@ -83,6 +85,34 @@ const paid = (providers: Record (language as { config: { baseURL: string } }).config.baseURL +const aggregatorExamplePath = path.join(import.meta.dir, "../../../../docs/examples/internal-llm-aggregator.jsonc") +const aggregatorExample = ConfigParse.schema( + ConfigV1.Info, + ConfigParse.jsonc(await Bun.file(aggregatorExamplePath).text(), aggregatorExamplePath), + aggregatorExamplePath, +) + +const emptyModelCatalogLayer = Layer.succeed( + ModelsDev.Service, + ModelsDev.Service.of({ + get: () => Effect.succeed({}), + refresh: () => Effect.dieMessage("models.dev refresh must not be called by the aggregator contract"), + }), +) + +const aggregatorProviderLayer = Provider.layer.pipe( + Layer.provide(FSUtil.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(Auth.defaultLayer), + Layer.provide(Plugin.defaultLayer), + Layer.provide(emptyModelCatalogLayer), + Layer.provide(RuntimeFlags.layer({})), +) +const aggregator = testEffect( + Layer.mergeAll(aggregatorProviderLayer, Env.defaultLayer, Plugin.defaultLayer, emptyModelCatalogLayer), +) + const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer)) const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true })) @@ -394,6 +424,52 @@ it.instance( }, ) +aggregator.instance( + "internal aggregator example resolves three protocol SDKs without models.dev", + Effect.gen(function* () { + yield* set("LLM_AGGREGATOR_API_KEY", "internal-test-key") + const provider = yield* Provider.Service + const providers = yield* provider.list() + const modelsDev = yield* ModelsDev.Service + + expect(yield* modelsDev.get()).toEqual({}) + + yield* Effect.forEach( + [ + { + providerID: ProviderV2.ID.make("internal-openai"), + modelID: ModelV2.ID.make("gpt-5"), + npm: "@ai-sdk/openai", + baseURL: "https://llm-api.example.internal/openai/v1", + }, + { + providerID: ProviderV2.ID.make("internal-anthropic"), + modelID: ModelV2.ID.make("claude-sonnet-4-20250514"), + npm: "@ai-sdk/anthropic", + baseURL: "https://llm-api.example.internal/anthropic", + }, + { + providerID: ProviderV2.ID.make("internal-gemini"), + modelID: ModelV2.ID.make("gemini-2.5-pro"), + npm: "@ai-sdk/google", + baseURL: "https://llm-api.example.internal/gemini/v1beta", + }, + ], + (item) => + Effect.gen(function* () { + expect(providers[item.providerID]).toBeDefined() + expect(providers[item.providerID].key).toBe("internal-test-key") + expect(providers[item.providerID].options.baseURL).toBe(item.baseURL) + expect(providers[item.providerID].models[item.modelID].api.npm).toBe(item.npm) + + const model = yield* provider.getModel(item.providerID, item.modelID) + expect(yield* provider.getLanguage(model)).toBeDefined() + }), + ) + }), + { config: aggregatorExample }, +) + it.instance( "model cost defaults to zero when not specified", Effect.gen(function* () { From dae9a9e86d2949cbb6f3613411d35d0da413081d Mon Sep 17 00:00:00 2001 From: Lex Date: Thu, 13 Aug 2026 20:22:14 +0800 Subject: [PATCH 2/2] fixup! test(provider): cover internal aggregator endpoints --- packages/opencode/test/provider/provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 84ae9fddee..9f5de9c2aa 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -96,7 +96,7 @@ const emptyModelCatalogLayer = Layer.succeed( ModelsDev.Service, ModelsDev.Service.of({ get: () => Effect.succeed({}), - refresh: () => Effect.dieMessage("models.dev refresh must not be called by the aggregator contract"), + refresh: () => Effect.die(new Error("models.dev refresh must not be called by the aggregator contract")), }), )