diff --git a/packages/ai-provider-bridge/src/providers/__tests__/positai-provider.test.ts b/packages/ai-provider-bridge/src/providers/__tests__/positai-provider.test.ts index 50499da..920e8b2 100644 --- a/packages/ai-provider-bridge/src/providers/__tests__/positai-provider.test.ts +++ b/packages/ai-provider-bridge/src/providers/__tests__/positai-provider.test.ts @@ -18,6 +18,40 @@ function createMockLogger(): Logger { }; } +const OAUTH_CREDENTIALS = { + type: "oauth", + accessToken: "test-token", +} as ProviderCredentials; + +function createModelsResponse( + ids: string[], + protocol: string = "openai-chat-completions", +): Response { + return new Response( + JSON.stringify({ + chat: ids.map((id) => ({ + id, + display_name: id, + endpoints: [{ path: "/openai/v1", protocol }], + max_context_length: 200_000, + })), + }), + { status: 200 }, + ); +} + +async function fetchModels(ids: string[], protocol?: string) { + const logger = createMockLogger(); + const registry = new ProviderRegistry(logger); + registerPositAiProvider(registry, "https://api.posit.cloud", "test/1.0", logger); + + vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(createModelsResponse(ids, protocol)); + + const models = await registry.getModelsForProvider("positai", OAUTH_CREDENTIALS); + vi.restoreAllMocks(); + return models; +} + describe("Posit AI protocol mapping", () => { it("maps anthropic-messages protocol and sets vendor to anthropic", async () => { const logger = createMockLogger(); @@ -120,3 +154,23 @@ describe("Posit AI protocol mapping", () => { vi.restoreAllMocks(); }); }); + +describe("Posit AI maxOutputTokens fallback", () => { + it("applies the fallback maxOutputTokens to a model with no capability entry", async () => { + const models = await fetchModels(["some-org/unknown-model"]); + expect(models).toHaveLength(1); + expect(models[0]?.maxOutputTokens).toBe(16_384); + }); + + it("applies the fallback maxOutputTokens to GLM-5.2, whose capability entry omits it", async () => { + const models = await fetchModels(["zai-org/GLM-5.2"]); + expect(models).toHaveLength(1); + expect(models[0]?.maxOutputTokens).toBe(16_384); + }); + + it("lets an explicit capability-entry maxOutputTokens take precedence (Kimi K3)", async () => { + const models = await fetchModels(["moonshotai/Kimi-K3"]); + expect(models).toHaveLength(1); + expect(models[0]?.maxOutputTokens).toBe(131_072); + }); +}); diff --git a/packages/ai-provider-bridge/src/providers/positai-provider.ts b/packages/ai-provider-bridge/src/providers/positai-provider.ts index a63463e..da067e2 100644 --- a/packages/ai-provider-bridge/src/providers/positai-provider.ts +++ b/packages/ai-provider-bridge/src/providers/positai-provider.ts @@ -176,8 +176,11 @@ export function registerPositAiProvider( "image/webp", "application/pdf", ], - // No blanket maxOutputTokens: each model family declares its own - // in its capability entry; otherwise the server default applies. + // Fallback maxOutputTokens for models whose capability entry omits + // one: without it no max_tokens is sent and the server applies its + // own (much smaller) default. Explicit per-model values in the + // capabilities spread below take precedence. + maxOutputTokens: 16_384, ...capabilities, // API-sourced context length takes precedence over inferred capabilities maxContextLength: model.max_context_length ?? capabilities?.maxContextLength ?? 200000,