From c6352cf71f29bea344b3e3d08dc28e124466d8dd Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Sat, 10 Oct 2026 20:01:24 -0400 Subject: [PATCH] refactor(ai-chat): drop provider surface and OpenAIUtil code nothing uses - list() leaves IChatProvider and every provider that isn't Azure or xAI (both wait on the open fix for those files); only tests called it. - OpenAI Chat, OpenAI Responses and Meta no longer inline puter_path parts: the driver does it before every attempt, so theirs found nothing to do. The stores and FS service they took for it go too. - providers/ChatProvider.ts had no subclasses. - OpenAIUtil: one hoistToolUseBlocks for the Chat and Responses message passes (Responses keeps canonical_id), and the dead create_usage_calculator, unused Responses deviations and constant mode/toolblock are gone. checkModeration stays on IChatProvider: the open Azure moderation fix calls it through that interface. --- .../drivers/ai-chat/ChatCompletionDriver.ts | 21 +-- .../drivers/ai-chat/providers/ChatProvider.ts | 44 ------ .../ai-chat/providers/FakeChatProvider.ts | 4 +- .../ai-chat/providers/OpenAICompatProvider.ts | 13 +- .../providers/alibaba/AlibabaProvider.test.ts | 5 +- .../byteplus/BytePlusProvider.test.ts | 7 +- .../providers/claude/ClaudeProvider.test.ts | 7 +- .../providers/claude/ClaudeProvider.ts | 5 - .../deepseek/DeepSeekProvider.test.ts | 5 +- .../gemini/GeminiChatProvider.test.ts | 7 +- .../providers/groq/GroqAIProvider.test.ts | 5 +- .../providers/hoonify/HoonifyProvider.test.ts | 5 +- .../providers/infron/InfronProvider.test.ts | 5 +- .../meta/MetaProvider.integration.test.ts | 12 +- .../providers/meta/MetaProvider.test.ts | 12 +- .../ai-chat/providers/meta/MetaProvider.ts | 26 +--- .../providers/minimax/MiniMaxProvider.test.ts | 3 +- .../mistral/MistralAiProvider.test.ts | 7 +- .../providers/mistral/MistralAiProvider.ts | 5 - .../moonshot/MoonshotProvider.test.ts | 3 +- .../neuralwatt/NeuralwattProvider.test.ts | 3 +- .../providers/ollama/OllamaProvider.test.ts | 3 +- ...hatCompletionsProvider.integration.test.ts | 4 +- .../OpenAiChatCompletionsProvider.test.ts | 10 +- .../openai/OpenAiChatCompletionsProvider.ts | 32 +--- .../OpenAiChatResponsesProvider.test.ts | 12 +- .../openai/OpenAiChatResponsesProvider.ts | 31 +--- .../openrouter/OpenRouterProvider.test.ts | 5 +- .../providers/providerConsistency.test.ts | 12 +- .../together/TogetherAIProvider.test.ts | 3 +- .../ai-chat/providers/zai/ZAIProvider.test.ts | 3 +- src/backend/drivers/ai-chat/types.ts | 1 - .../drivers/ai-chat/utils/OpenAIUtil.js | 141 +++++------------- .../drivers/ai-chat/utils/OpenAIUtil.test.ts | 31 ---- 34 files changed, 108 insertions(+), 384 deletions(-) delete mode 100644 src/backend/drivers/ai-chat/providers/ChatProvider.ts diff --git a/src/backend/drivers/ai-chat/ChatCompletionDriver.ts b/src/backend/drivers/ai-chat/ChatCompletionDriver.ts index 1370b8ba8..cf2cc5eec 100644 --- a/src/backend/drivers/ai-chat/ChatCompletionDriver.ts +++ b/src/backend/drivers/ai-chat/ChatCompletionDriver.ts @@ -1626,23 +1626,13 @@ export class ChatCompletionDriver extends PuterDriver { const openaiKey = readKey(providers['openai-completion']); if (openaiKey) { - const openaiStores = { - fsEntry: this.stores.fsEntry, - s3Object: this.stores.s3Object, - }; const openaiCompletions = new OpenAiChatProvider( + { apiKey: openaiKey }, metering, - openaiStores, - this.services.fs, - { - apiKey: openaiKey, - }, ); const openaiResponses = new OpenAiResponsesChatProvider( - metering, - openaiStores, - this.services.fs, { apiKey: openaiKey }, + metering, ); // web_search is Responses-only; let the Completions path delegate // to its sibling when users request it. @@ -1662,16 +1652,11 @@ export class ChatCompletionDriver extends PuterDriver { const metaKey = readKey(meta); if (metaKey) { this.#providers['meta'] = new MetaProvider( - metering, - { - fsEntry: this.stores.fsEntry, - s3Object: this.stores.s3Object, - }, - this.services.fs, { apiKey: metaKey, apiBaseUrl: meta?.apiBaseUrl as string | undefined, }, + metering, ); } diff --git a/src/backend/drivers/ai-chat/providers/ChatProvider.ts b/src/backend/drivers/ai-chat/providers/ChatProvider.ts deleted file mode 100644 index 9c83dee28..000000000 --- a/src/backend/drivers/ai-chat/providers/ChatProvider.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import type { - IChatModel, - IChatProvider, - ICompleteArguments, - IChatCompleteResult, -} from '../types.js'; - -/** - * Abstract base for AI chat providers. Each provider wraps a single upstream - * API (Anthropic, OpenAI, …) and exposes the unified `IChatProvider` contract. - */ -export class ChatProvider implements IChatProvider { - getDefaultModel(): string { - return ''; - } - models(): IChatModel[] | Promise { - return []; - } - list(): string[] | Promise { - return []; - } - async complete(_arg: ICompleteArguments): Promise { - throw new Error('Method not implemented.'); - } -} diff --git a/src/backend/drivers/ai-chat/providers/FakeChatProvider.ts b/src/backend/drivers/ai-chat/providers/FakeChatProvider.ts index c4a13c5b7..f1c9024b7 100644 --- a/src/backend/drivers/ai-chat/providers/FakeChatProvider.ts +++ b/src/backend/drivers/ai-chat/providers/FakeChatProvider.ts @@ -74,9 +74,7 @@ export class FakeChatProvider implements IChatProvider { }, ]; } - async list() { - return ['fake', 'costly', 'abuse']; - } + async complete({ messages, stream, diff --git a/src/backend/drivers/ai-chat/providers/OpenAICompatProvider.ts b/src/backend/drivers/ai-chat/providers/OpenAICompatProvider.ts index 8c95de903..dfecbdc27 100644 --- a/src/backend/drivers/ai-chat/providers/OpenAICompatProvider.ts +++ b/src/backend/drivers/ai-chat/providers/OpenAICompatProvider.ts @@ -40,7 +40,7 @@ import { meterChatUsage, type MeterChatUsageOptions, } from '../utils/meterChatUsage.js'; -import { modelLookupNames, normalizeModelKey } from '../utils/modelRouting.js'; +import { normalizeModelKey } from '../utils/modelRouting.js'; import * as OpenAIUtil from '../utils/OpenAIUtil.js'; import { openAICompatParams, @@ -140,17 +140,6 @@ export class OpenAICompatProvider implements IChatProvider { return this.#options.models?.() ?? []; } - list(): string[] | Promise { - const models = this.models(); - return Array.isArray(models) - ? modelLookupNames(models) - : this.#listAsync(models); - } - - async #listAsync(models: Promise) { - return modelLookupNames(await models); - } - /** The model key this provider records usage under. */ meteringModelKey(modelId: string): string { const prefix = this.#options.meteringPrefix; diff --git a/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts b/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts index 6986d2d26..cbb2c86a1 100644 --- a/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts @@ -50,6 +50,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { ALIBABA_MODELS } from './models.js'; import { AlibabaProvider } from './AlibabaProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -170,7 +171,7 @@ describe('AlibabaProvider model catalog', () => { it('list() flattens canonical ids and aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); for (const m of ALIBABA_MODELS) { expect(ids).toContain(m.id); for (const a of m.aliases ?? []) { @@ -209,7 +210,7 @@ describe('AlibabaProvider model catalog', () => { 'deepseek-v4-flash', ])('drops %s, which Model Studio is retiring', async (id) => { const { provider } = makeProvider(); - expect(await provider.list()).not.toContain(id); + expect(modelLookupNames(await provider.models())).not.toContain(id); }); }); diff --git a/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts b/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts index 9e3f05de8..18ca778c3 100644 --- a/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/byteplus/BytePlusProvider.test.ts @@ -51,6 +51,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { BytePlusProvider } from './BytePlusProvider.js'; import { BYTEPLUS_MODELS } from './models.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // -- OpenAI SDK mock ------------------------------------------------- // @@ -190,7 +191,7 @@ describe('BytePlusProvider model catalog', () => { it('list() flattens canonical ids and aliases', () => { const { provider } = makeProvider(); - const names = provider.list(); + const names = modelLookupNames(provider.models()); for (const m of BYTEPLUS_MODELS) { expect(names).toContain(m.id); for (const a of m.aliases ?? []) { @@ -228,12 +229,12 @@ describe('BytePlusProvider model catalog', () => { 'gpt-oss-120b-250805', ])('drops %s, which ModelArk has deprecated', (id) => { const { provider } = makeProvider(); - expect(provider.list()).not.toContain(id); + expect(modelLookupNames(provider.models())).not.toContain(id); }); it('never claims the bare deepseek-v4 names owned by the DeepSeek provider', () => { const { provider } = makeProvider(); - const names = provider.list(); + const names = modelLookupNames(provider.models()); expect(names).not.toContain('deepseek-v4-pro'); expect(names).not.toContain('deepseek-v4-flash'); expect(names).toContain('byteplus/deepseek-v4-pro'); diff --git a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts index 2de9d3c9a..ad70ada9e 100644 --- a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.test.ts @@ -58,6 +58,7 @@ import { AIChatStream } from '../../utils/Streaming.js'; import { FILES_API_BETA } from './fileUpload.js'; import { CLAUDE_MODELS } from './models.js'; import { ClaudeProvider } from './ClaudeProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── Anthropic SDK mock ────────────────────────────────────────────── @@ -281,7 +282,7 @@ describe('ClaudeProvider model catalog', () => { it('list() flattens canonical ids and aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); for (const m of CLAUDE_MODELS) { expect(ids).toContain(m.id); for (const a of m.aliases ?? []) { @@ -302,7 +303,7 @@ describe('ClaudeProvider model catalog', () => { it('drops the deprecated claude-sonnet-4-5 and its aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).not.toContain('claude-sonnet-4-5-20250929'); expect(ids).not.toContain('claude-sonnet-4-5'); expect(ids).not.toContain('claude-sonnet-4.5'); @@ -1357,7 +1358,7 @@ describe('ClaudeProvider.complete non-stream output', () => { messages: [{ role: 'user', content: 'hi' }], }), ); - expect(await provider.list()).toContain(model); + expect(modelLookupNames(await provider.models())).toContain(model); const [args] = messagesCreateMock.mock.calls[0]!; expect(args.model).toBe(canonicalId); expect(args.max_tokens).toBe(128_000); diff --git a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts index 6a82d6424..9a1c3387f 100644 --- a/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts +++ b/src/backend/drivers/ai-chat/providers/claude/ClaudeProvider.ts @@ -48,7 +48,6 @@ import type { AIChatTextStream, AIChatToolUseStream, } from '../../utils/Streaming.js'; -import { modelLookupNames } from '../../utils/modelRouting.js'; import { allowlistedFromHeader, applySafeguardsPolicy, @@ -294,10 +293,6 @@ export class ClaudeProvider implements IChatProvider { return CLAUDE_MODELS; } - async list() { - return modelLookupNames(this.models()); - } - /** The model key this provider records usage under. */ meteringModelKey(modelId: string): string { return `claude:${modelId}`; diff --git a/src/backend/drivers/ai-chat/providers/deepseek/DeepSeekProvider.test.ts b/src/backend/drivers/ai-chat/providers/deepseek/DeepSeekProvider.test.ts index cf9ebf105..9c07df523 100644 --- a/src/backend/drivers/ai-chat/providers/deepseek/DeepSeekProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/deepseek/DeepSeekProvider.test.ts @@ -50,6 +50,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { DEEPSEEK_MODELS } from './models.js'; import { DeepSeekProvider } from './DeepSeekProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -159,7 +160,7 @@ describe('DeepSeekProvider model catalog', () => { it('list() flattens canonical ids and aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); for (const m of DEEPSEEK_MODELS) { expect(ids).toContain(m.id); for (const a of m.aliases ?? []) { @@ -489,7 +490,7 @@ describe('DeepSeekProvider model resolution', () => { 'deepseek:deepseek/deepseek-reasoner', ])('drops the discontinued %s instead of redirecting it', async (alias) => { const { provider } = makeProvider(); - expect(await provider.list()).not.toContain(alias); + expect(modelLookupNames(await provider.models())).not.toContain(alias); }); }); diff --git a/src/backend/drivers/ai-chat/providers/gemini/GeminiChatProvider.test.ts b/src/backend/drivers/ai-chat/providers/gemini/GeminiChatProvider.test.ts index 93072f948..0c6e369d3 100644 --- a/src/backend/drivers/ai-chat/providers/gemini/GeminiChatProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/gemini/GeminiChatProvider.test.ts @@ -51,6 +51,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { GEMINI_MODELS } from './models.js'; import { GeminiChatProvider } from './GeminiChatProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── // @@ -172,7 +173,7 @@ describe('GeminiChatProvider model catalog', () => { it('list() flattens canonical ids and aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); for (const m of GEMINI_MODELS) { expect(ids).toContain(m.id); for (const a of m.aliases ?? []) { @@ -192,7 +193,7 @@ describe('GeminiChatProvider model catalog', () => { // flattening the catalog itself. it('list() emits every id exactly once', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).toHaveLength(new Set(ids).size); }); }); @@ -413,7 +414,7 @@ describe('GeminiChatProvider model resolution', () => { 'gemini-3-flash-preview', ])('drops the deprecated %s instead of redirecting it', async (model) => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).not.toContain(model); expect(ids).not.toContain(`google/${model}`); }); diff --git a/src/backend/drivers/ai-chat/providers/groq/GroqAIProvider.test.ts b/src/backend/drivers/ai-chat/providers/groq/GroqAIProvider.test.ts index 2660a5997..e0196dde4 100644 --- a/src/backend/drivers/ai-chat/providers/groq/GroqAIProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/groq/GroqAIProvider.test.ts @@ -51,6 +51,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { GROQ_MODELS } from './models.js'; import { GroqAIProvider } from './GroqAIProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── Groq SDK mock ─────────────────────────────────────────────────── @@ -163,7 +164,7 @@ describe('GroqAIProvider model catalog', () => { it('list() flattens canonical ids and aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); for (const m of GROQ_MODELS) { expect(ids).toContain(m.id); for (const a of m.aliases ?? []) { @@ -195,7 +196,7 @@ describe('GroqAIProvider model catalog', () => { 'meta-llama/llama-guard-4-12b', ])('drops the deprecated %s', async (id) => { const { provider } = makeProvider(); - expect(await provider.list()).not.toContain(id); + expect(modelLookupNames(await provider.models())).not.toContain(id); }); }); diff --git a/src/backend/drivers/ai-chat/providers/hoonify/HoonifyProvider.test.ts b/src/backend/drivers/ai-chat/providers/hoonify/HoonifyProvider.test.ts index 463f30c53..b6b13d63d 100644 --- a/src/backend/drivers/ai-chat/providers/hoonify/HoonifyProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/hoonify/HoonifyProvider.test.ts @@ -52,6 +52,7 @@ import { AIChatStream } from '../../utils/Streaming.js'; import { usdPerMToken } from '../../utils/pricing.js'; import { HOONIFY_MODELS } from './models.js'; import { HoonifyProvider } from './HoonifyProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── // @@ -191,7 +192,7 @@ describe('HoonifyProvider model catalog', () => { it('list() flattens canonical ids and aliases', () => { const { provider } = makeProvider(); - const names = provider.list(); + const names = modelLookupNames(provider.models()); for (const m of HOONIFY_MODELS) { expect(names).toContain(m.id); for (const a of m.aliases ?? []) { @@ -216,7 +217,7 @@ describe('HoonifyProvider model catalog', () => { // `AGGREGATOR_PROVIDERS` ranks Hoonify behind the vendor so it only // serves once Alibaba's route fails. const { provider } = makeProvider(); - expect(provider.list()).toContain('qwen/qwen3.6-27b'); + expect(modelLookupNames(provider.models())).toContain('qwen/qwen3.6-27b'); }); it('exposes Inkling Small at its list pricing and sends its exact-case wire id', async () => { diff --git a/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts b/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts index 726b1d56c..14190c31e 100644 --- a/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/infron/InfronProvider.test.ts @@ -50,6 +50,7 @@ import { setupTestServer } from '../../../../testUtil.js'; import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { InfronProvider } from './InfronProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -331,7 +332,7 @@ describe('InfronProvider model catalog', () => { it('list() prefixes ids with infron: and filters non-chat, display-only, and deprecated entries', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).toContain('infron:deepseek/deepseek-v4-flash'); expect(ids).toContain(provider.getDefaultModel()); expect(ids).toContain('infron:anthropic/claude-haiku-4.5'); @@ -417,7 +418,7 @@ describe('InfronProvider model catalog', () => { it('still offers an explicit tier id when no default tier is sold', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); // The plain id leaves routing to Infron; the :flex id pins the one // tier on offer. Same price, different routing guarantee. expect(ids).toContain('infron:example/flex-only-model'); diff --git a/src/backend/drivers/ai-chat/providers/meta/MetaProvider.integration.test.ts b/src/backend/drivers/ai-chat/providers/meta/MetaProvider.integration.test.ts index 127612114..ce4896f30 100644 --- a/src/backend/drivers/ai-chat/providers/meta/MetaProvider.integration.test.ts +++ b/src/backend/drivers/ai-chat/providers/meta/MetaProvider.integration.test.ts @@ -27,9 +27,6 @@ */ import { describe, expect, it } from 'vitest'; -import type { FSService } from '../../../../services/fs/FSService.js'; -import type { FSEntryStore } from '../../../../stores/fs/FSEntryStore.js'; -import type { S3ObjectStore } from '../../../../stores/fs/S3ObjectStore.js'; import { INTEGRATION_TEST_TIMEOUT_MS, makeMeteringStub, @@ -41,21 +38,14 @@ import { MetaProvider } from './MetaProvider.js'; const ENV_VAR = 'PUTER_TEST_AI_META_API_KEY'; -// Only `puter_path` content parts reach the filesystem, and this test sends -// plain text, so the stores stay untouched. -const UNUSED_STORES = {} as { fsEntry: FSEntryStore; s3Object: S3ObjectStore }; -const UNUSED_FS_SERVICE = {} as FSService; - describe.skipIf(skipUnlessEnv(ENV_VAR))('MetaProvider (integration)', () => { it( 'returns a non-empty completion from muse-spark-1.2', { timeout: INTEGRATION_TEST_TIMEOUT_MS }, async () => { const provider = new MetaProvider( - makeMeteringStub(), - UNUSED_STORES, - UNUSED_FS_SERVICE, { apiKey: optionalEnv(ENV_VAR)! }, + makeMeteringStub(), ); const result = await withTestActor(() => diff --git a/src/backend/drivers/ai-chat/providers/meta/MetaProvider.test.ts b/src/backend/drivers/ai-chat/providers/meta/MetaProvider.test.ts index e8e3bf896..980e024ac 100644 --- a/src/backend/drivers/ai-chat/providers/meta/MetaProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/meta/MetaProvider.test.ts @@ -52,6 +52,7 @@ import { AIChatStream } from '../../utils/Streaming.js'; import { usdPerMToken } from '../../utils/pricing.js'; import { MetaProvider } from './MetaProvider.js'; import { META_MODELS } from './models.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // -- OpenAI SDK mock ---------------------------------------------- // @@ -93,16 +94,11 @@ afterAll(async () => { const makeProvider = (config: { apiKey?: string; apiBaseUrl?: string } = {}) => new MetaProvider( - server.services.metering, - { - fsEntry: server.stores.fsEntry, - s3Object: server.stores.s3Object, - }, - server.services.fs, { apiKey: config.apiKey ?? 'test-key', ...(config.apiBaseUrl ? { apiBaseUrl: config.apiBaseUrl } : {}), }, + server.services.metering, ); const asAsyncIterable = (items: T[]): AsyncIterable => ({ @@ -207,7 +203,7 @@ describe('MetaProvider model catalog', () => { }); it('list() flattens canonical ids and aliases', () => { - const names = makeProvider().list(); + const names = modelLookupNames(makeProvider().models()); for (const m of META_MODELS) { expect(names).toContain(m.id); for (const a of m.aliases ?? []) { @@ -237,7 +233,7 @@ describe('MetaProvider model catalog', () => { // would win bucket routing — and Meta trains on what it serves. It is // also gated behind a separate enrolment, so a standard-tier key gets // `model_not_found` for it. - const names = new Set(makeProvider().list()); + const names = new Set(modelLookupNames(makeProvider().models())); for (const name of names) { expect(name).not.toContain('contributor'); } diff --git a/src/backend/drivers/ai-chat/providers/meta/MetaProvider.ts b/src/backend/drivers/ai-chat/providers/meta/MetaProvider.ts index b257f0cb6..56494cec7 100644 --- a/src/backend/drivers/ai-chat/providers/meta/MetaProvider.ts +++ b/src/backend/drivers/ai-chat/providers/meta/MetaProvider.ts @@ -19,12 +19,8 @@ import { OpenAI } from 'openai'; import type { Actor } from '../../../../core/actor.js'; -import { Context } from '../../../../core/context.js'; import { HttpError } from '../../../../core/http/HttpError.js'; -import type { FSService } from '../../../../services/fs/FSService.js'; import type { MeteringService } from '../../../../services/metering/MeteringService.js'; -import type { FSEntryStore } from '../../../../stores/fs/FSEntryStore.js'; -import type { S3ObjectStore } from '../../../../stores/fs/S3ObjectStore.js'; import { upstreamUserIdentifier } from '../../../util/upstreamIdentifier.js'; import type { IChatModel, ICompleteArguments } from '../../types.js'; import { sdkClientOptions } from '../../utils/sdkClient.js'; @@ -32,7 +28,6 @@ import { type ChatProviderConfig, OpenAICompatProvider, } from '../OpenAICompatProvider.js'; -import { processPuterPathUploads } from '../openai/fileUpload.js'; import { META_MODELS, MUSE_SPARK_DEFAULT_MODEL } from './models.js'; const DEFAULT_API_BASE_URL = 'https://api.meta.ai/v1'; @@ -61,16 +56,7 @@ const asRecord = (value: unknown): Record => * models behind Responses- and Anthropic-Messages-shaped endpoints. */ export class MetaProvider extends OpenAICompatProvider { - #stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore }; - - #fsService: FSService; - - constructor( - meteringService: MeteringService, - stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore }, - fsService: FSService, - config: ChatProviderConfig, - ) { + constructor(config: ChatProviderConfig, meteringService: MeteringService) { super(meteringService, { client: new OpenAI({ apiKey: config.apiKey, @@ -89,8 +75,6 @@ export class MetaProvider extends OpenAICompatProvider { stripAnthropicShape: true, compatParams: { toolChoiceAutoOnly: true }, }); - this.#stores = stores; - this.#fsService = fsService; } override async complete(args: ICompleteArguments, resolved?: IChatModel) { @@ -99,14 +83,6 @@ export class MetaProvider extends OpenAICompatProvider { legacyCode: 'bad_request', }); } - // Muse Spark reads images, video, PDFs and audio, but Chat Completions - // takes them inline only — resolve `puter_path` parts to data URLs. - await processPuterPathUploads( - args.messages, - this.#stores, - this.#fsService, - Context.get('actor'), - ); return super.complete(args, resolved); } diff --git a/src/backend/drivers/ai-chat/providers/minimax/MiniMaxProvider.test.ts b/src/backend/drivers/ai-chat/providers/minimax/MiniMaxProvider.test.ts index e41f1301e..084378877 100644 --- a/src/backend/drivers/ai-chat/providers/minimax/MiniMaxProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/minimax/MiniMaxProvider.test.ts @@ -36,6 +36,7 @@ import { setupTestServer } from '../../../../testUtil.js'; import { withTestActor } from '../../../integrationTestUtil.js'; import { MINIMAX_MODELS } from './models.js'; import { MiniMaxProvider } from './MiniMaxProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; const { createMock, openAICtor } = vi.hoisted(() => { const createMock = vi.fn(); @@ -133,7 +134,7 @@ describe('MiniMaxProvider model catalog', () => { it('list() flattens canonical ids and aliases', () => { const { provider } = makeProvider(); - const ids = provider.list(); + const ids = modelLookupNames(provider.models()); for (const model of MINIMAX_MODELS) { expect(ids).toContain(model.id); for (const alias of model.aliases ?? []) { diff --git a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts index fe2429938..ae135e05c 100644 --- a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.test.ts @@ -54,6 +54,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { MISTRAL_MODELS } from './models.js'; import { MistralAIProvider } from './MistralAiProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── Mistral SDK mock ──────────────────────────────────────────────── // @@ -166,7 +167,7 @@ describe('MistralAIProvider model catalog', () => { it('list() flattens canonical ids and aliases', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); for (const m of MISTRAL_MODELS) { expect(ids).toContain(m.id); for (const a of m.aliases ?? []) { @@ -180,14 +181,14 @@ describe('MistralAIProvider model catalog', () => { it('does not redirect the retired Magistral ids to another model', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).not.toContain('magistral-small-latest'); expect(ids).not.toContain('magistral-medium-latest'); }); it('keeps mistral-large-2512, which Mistral still serves as active', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).toContain('mistral-large-2512'); expect(ids).toContain('mistral-large-latest'); }); diff --git a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts index 0e639f911..2d2bc4b7e 100644 --- a/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts +++ b/src/backend/drivers/ai-chat/providers/mistral/MistralAiProvider.ts @@ -36,7 +36,6 @@ import { } from '../../utils/mediaParts.js'; import * as OpenAIUtil from '../../utils/OpenAIUtil.js'; import { MISTRAL_MODELS } from './models.js'; -import { modelLookupNames } from '../../utils/modelRouting.js'; import { shouldPresentAsOpenAI } from '../../utils/normalizeToOpenAI.js'; import { withSdkTimeout } from '../../utils/sdkClient.js'; import { meterChatUsage } from '../../utils/meterChatUsage.js'; @@ -130,10 +129,6 @@ export class MistralAIProvider implements IChatProvider { return MISTRAL_MODELS; } - async list() { - return modelLookupNames(await this.models()); - } - /** * The Mistral SDK validates camelCase fields, so canonical `image_url` * parts become `imageUrl` (a snake_case key fails its schema client-side). diff --git a/src/backend/drivers/ai-chat/providers/moonshot/MoonshotProvider.test.ts b/src/backend/drivers/ai-chat/providers/moonshot/MoonshotProvider.test.ts index ec50e3ece..6a23081a5 100644 --- a/src/backend/drivers/ai-chat/providers/moonshot/MoonshotProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/moonshot/MoonshotProvider.test.ts @@ -54,6 +54,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { MOONSHOT_MODELS } from './models.js'; import { MoonshotProvider } from './MoonshotProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -180,7 +181,7 @@ describe('MoonshotProvider model catalog', () => { it('list() flattens canonical ids and aliases (returned via async)', async () => { const { provider } = makeProvider(); - const names = await provider.list(); + const names = modelLookupNames(await provider.models()); for (const m of MOONSHOT_MODELS) { expect(names).toContain(m.id); for (const a of m.aliases ?? []) { diff --git a/src/backend/drivers/ai-chat/providers/neuralwatt/NeuralwattProvider.test.ts b/src/backend/drivers/ai-chat/providers/neuralwatt/NeuralwattProvider.test.ts index 56ee6136f..70480664a 100644 --- a/src/backend/drivers/ai-chat/providers/neuralwatt/NeuralwattProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/neuralwatt/NeuralwattProvider.test.ts @@ -49,6 +49,7 @@ import { stripNeuralwattPrefix, } from './models.js'; import { NeuralwattProvider } from './NeuralwattProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -336,7 +337,7 @@ describe('NeuralwattProvider model catalog', () => { it('list() prefixes ids and skips deprecated / pricing_tbd entries', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).toContain('neuralwatt:deepseek-v4-flash'); expect(ids).toContain('neuralwatt:zai-org/GLM-5.1-FP8'); expect(ids).toContain('GLM-5.1-FP8'); diff --git a/src/backend/drivers/ai-chat/providers/ollama/OllamaProvider.test.ts b/src/backend/drivers/ai-chat/providers/ollama/OllamaProvider.test.ts index cf9daf0f7..8c68bcfbb 100644 --- a/src/backend/drivers/ai-chat/providers/ollama/OllamaProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/ollama/OllamaProvider.test.ts @@ -46,6 +46,7 @@ import { setupTestServer } from '../../../../testUtil.js'; import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { OllamaChatProvider } from './OllamaProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // -- External boundaries --------------------------------------------- @@ -233,7 +234,7 @@ describe('OllamaChatProvider model discovery', () => { axiosRequestMock.mockResolvedValueOnce({ data: { models: [{ name: 'llama3.2' }, { name: 'qwen3' }] }, }); - expect(await makeProvider().list()).toEqual([ + expect(modelLookupNames(await makeProvider().models())).toEqual([ 'ollama:ollama/llama3.2', 'ollama:ollama/qwen3', ]); diff --git a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.integration.test.ts b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.integration.test.ts index a1c1716bc..53231a77f 100644 --- a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.integration.test.ts +++ b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.integration.test.ts @@ -44,10 +44,8 @@ describe.skipIf(skipUnlessEnv(ENV_VAR))( () => { const buildProvider = () => new OpenAiChatProvider( - makeMeteringStub(), - { fsEntry: undefined as never, s3Object: undefined as never }, - undefined as never, { apiKey: optionalEnv(ENV_VAR)! }, + makeMeteringStub(), ); it('returns a non-empty completion from gpt-4o-mini', { timeout: INTEGRATION_TEST_TIMEOUT_MS }, async () => { diff --git a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.test.ts b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.test.ts index 277ce952f..04869e1ab 100644 --- a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.test.ts @@ -56,6 +56,7 @@ import { import { AIChatStream } from '../../utils/Streaming.js'; import { OPEN_AI_MODELS } from './models.js'; import { OpenAiChatProvider } from './OpenAiChatCompletionsProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -92,13 +93,8 @@ afterAll(async () => { const makeProvider = () => { const provider = new OpenAiChatProvider( - server.services.metering, - { - fsEntry: server.stores.fsEntry, - s3Object: server.stores.s3Object, - }, - server.services.fs, { apiKey: 'test-key' }, + server.services.metering, ); return { provider }; }; @@ -187,7 +183,7 @@ describe('OpenAiChatProvider model catalog', () => { it('list() flattens canonical ids and aliases', () => { const { provider } = makeProvider(); - const ids = provider.list(); + const ids = modelLookupNames(provider.models()); expect(ids).toContain('gpt-5.2-2025-12-11'); expect(ids).toContain('gpt-5.2'); expect(ids).toContain('openai/gpt-5.2'); diff --git a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.ts b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.ts index 606782467..5238fef14 100644 --- a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.ts +++ b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatCompletionsProvider.ts @@ -21,10 +21,7 @@ import { HttpError } from '@heyputer/backend/src/core/http/HttpError.js'; import { OpenAI } from 'openai'; import { ChatCompletionCreateParams } from 'openai/resources/index.js'; import { Context } from '../../../../core/context.js'; -import type { FSService } from '../../../../services/fs/FSService.js'; import type { MeteringService } from '../../../../services/metering/MeteringService.js'; -import type { FSEntryStore } from '../../../../stores/fs/FSEntryStore.js'; -import type { S3ObjectStore } from '../../../../stores/fs/S3ObjectStore.js'; import type { IChatModel, IChatProvider, @@ -40,10 +37,8 @@ import { clampReasoningEffort, openAICompatParams, } from '../../utils/openaiParams.js'; -import { processPuterPathUploads } from './fileUpload.js'; import { OPEN_AI_MODELS } from './models.js'; import type { OpenAiResponsesChatProvider } from './OpenAiChatResponsesProvider.js'; -import { modelLookupNames } from '../../utils/modelRouting.js'; import { upstreamUserIdentifier } from '../../../util/upstreamIdentifier.js'; import { sdkClientOptions } from '../../utils/sdkClient.js'; import { meterChatUsage } from '../../utils/meterChatUsage.js'; @@ -70,21 +65,10 @@ export class OpenAiChatProvider implements IChatProvider { #meteringService: MeteringService; - #stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore }; - - #fsService: FSService; - #responsesProvider: OpenAiResponsesChatProvider | null = null; - constructor( - meteringService: MeteringService, - stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore }, - fsService: FSService, - config: { apiKey: string }, - ) { + constructor(config: { apiKey: string }, meteringService: MeteringService) { this.#meteringService = meteringService; - this.#stores = stores; - this.#fsService = fsService; this.#openAi = new OpenAI({ apiKey: config.apiKey, ...sdkClientOptions(), @@ -107,10 +91,6 @@ export class OpenAiChatProvider implements IChatProvider { return OPEN_AI_MODELS.filter((e) => !e.responses_api_only); } - list() { - return modelLookupNames(this.models()); - } - getDefaultModel() { return this.#defaultModel; } @@ -201,16 +181,6 @@ export class OpenAiChatProvider implements IChatProvider { // Cache key defaults to the actor identifier; see upstreamUserIdentifier. const cacheKey = prompt_cache_key ?? userIdentifier; - // Resolve any `puter_path` content parts into inline base64 data URLs. - // Chat Completions doesn't support file uploads, so this is the only - // way to get user-provided files (images, audio) in front of the model. - await processPuterPathUploads( - messages, - this.#stores, - this.#fsService, - actor, - ); - // Strip Anthropic-only shape a fallback-replayed message can carry // (thinking/server-tool blocks, cache_control, citations) before the // existing in-place coercion below, so that mutates only this pass's diff --git a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.test.ts b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.test.ts index 3301ea70c..577f288bb 100644 --- a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.test.ts @@ -57,6 +57,7 @@ import { import { AIChatStream } from '../../utils/Streaming.js'; import { OPEN_AI_MODELS } from './models.js'; import { OpenAiResponsesChatProvider } from './OpenAiChatResponsesProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -98,13 +99,8 @@ afterAll(async () => { const makeProvider = () => { const provider = new OpenAiResponsesChatProvider( - server.services.metering, - { - fsEntry: server.stores.fsEntry, - s3Object: server.stores.s3Object, - }, - server.services.fs, { apiKey: 'test-key' }, + server.services.metering, ); return { provider }; }; @@ -254,7 +250,7 @@ describe('OpenAiResponsesChatProvider model catalog', () => { max_tokens: 128_000, responses_api: true, }); - expect(provider.list()).toEqual( + expect(modelLookupNames(provider.models())).toEqual( expect.arrayContaining([id, `openai/${id}`]), ); }, @@ -272,7 +268,7 @@ describe('OpenAiResponsesChatProvider model catalog', () => { it('list() flattens canonical ids and aliases for Responses models', () => { const { provider } = makeProvider(); - const ids = provider.list(); + const ids = modelLookupNames(provider.models()); expect(ids).toContain('gpt-5.6-sol'); expect(ids).toContain('openai/gpt-5.6-sol'); expect(ids).toContain('gpt-6-astra'); diff --git a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.ts b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.ts index c9421d033..e88a4a59c 100644 --- a/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.ts +++ b/src/backend/drivers/ai-chat/providers/openai/OpenAiChatResponsesProvider.ts @@ -20,10 +20,7 @@ import { OpenAI } from 'openai'; import { ResponseCreateParams } from 'openai/resources/responses/responses.mjs'; import { Context } from '../../../../core/context.js'; -import type { FSService } from '../../../../services/fs/FSService.js'; import type { MeteringService } from '../../../../services/metering/MeteringService.js'; -import type { FSEntryStore } from '../../../../stores/fs/FSEntryStore.js'; -import type { S3ObjectStore } from '../../../../stores/fs/S3ObjectStore.js'; import type { IChatModel, IChatProvider, @@ -38,10 +35,8 @@ import { rejectStatefulResponsesFields, } from '../../utils/openaiParams.js'; import { responseSamplingParams } from '../../utils/responseSampling.js'; -import { processPuterPathUploads } from './fileUpload.js'; import { OPEN_AI_MODELS } from './models.js'; import { HttpError } from '@heyputer/backend/src/core/http/HttpError.js'; -import { modelLookupNames } from '../../utils/modelRouting.js'; import { upstreamUserIdentifier } from '../../../util/upstreamIdentifier.js'; import { sdkClientOptions } from '../../utils/sdkClient.js'; import { meterChatUsage } from '../../utils/meterChatUsage.js'; @@ -92,19 +87,8 @@ export class OpenAiResponsesChatProvider implements IChatProvider { #meteringService: MeteringService; - #stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore }; - - #fsService: FSService; - - constructor( - meteringService: MeteringService, - stores: { fsEntry: FSEntryStore; s3Object: S3ObjectStore }, - fsService: FSService, - config: { apiKey: string }, - ) { + constructor(config: { apiKey: string }, meteringService: MeteringService) { this.#meteringService = meteringService; - this.#stores = stores; - this.#fsService = fsService; this.#openAi = new OpenAI({ apiKey: config.apiKey, ...sdkClientOptions(), @@ -125,10 +109,6 @@ export class OpenAiResponsesChatProvider implements IChatProvider { ); } - list() { - return modelLookupNames(this.models({ no_restrictions: false })); - } - getDefaultModel() { return this.#defaultModel; } @@ -216,15 +196,6 @@ export class OpenAiResponsesChatProvider implements IChatProvider { // Cache key defaults to the actor identifier; see upstreamUserIdentifier. const cacheKey = prompt_cache_key ?? userIdentifier; - // Resolve any `puter_path` content parts into inline base64 data URLs - // before the Responses API sees them. - await processPuterPathUploads( - messages, - this.#stores, - this.#fsService, - actor, - ); - if (tools) { // Unravel tools to OpenAI Responses API format tools = (tools as any[]).map((e) => { diff --git a/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.test.ts b/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.test.ts index dabaf8947..f5106d5d2 100644 --- a/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/openrouter/OpenRouterProvider.test.ts @@ -50,6 +50,7 @@ import { setupTestServer } from '../../../../testUtil.js'; import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { OpenRouterProvider } from './OpenRouterProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── @@ -251,7 +252,7 @@ describe('OpenRouterProvider model catalog', () => { it('list() prefixes ids with openrouter: and filters out openrouter/auto', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).toContain('openrouter:openai/gpt-6-luna'); expect(ids).toContain('openrouter:anthropic/claude-haiku-4.5'); expect(ids).not.toContain('openrouter:openrouter/auto'); @@ -259,7 +260,7 @@ describe('OpenRouterProvider model catalog', () => { it('filters out models OpenRouter has scheduled for expiration', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); expect(ids).not.toContain('openrouter:qwen/qwen3-max'); expect(ids).not.toContain('qwen/qwen3-max'); }); diff --git a/src/backend/drivers/ai-chat/providers/providerConsistency.test.ts b/src/backend/drivers/ai-chat/providers/providerConsistency.test.ts index 679426e07..b0b7c51aa 100644 --- a/src/backend/drivers/ai-chat/providers/providerConsistency.test.ts +++ b/src/backend/drivers/ai-chat/providers/providerConsistency.test.ts @@ -259,9 +259,7 @@ const PROVIDERS: ProviderCase[] = [ name: 'meta', dialect: 'chat', make: () => - new MetaProvider(metering(), stores, fsService, { - apiKey: 'k', - } as never), + new MetaProvider({ apiKey: 'k' }, metering()), }, { name: 'minimax', @@ -301,17 +299,13 @@ const PROVIDERS: ProviderCase[] = [ name: 'openai-chat', dialect: 'chat', make: () => - new OpenAiChatProvider(metering(), stores, fsService, { - apiKey: 'k', - }), + new OpenAiChatProvider({ apiKey: 'k' }, metering()), }, { name: 'openai-responses', dialect: 'responses', make: () => - new OpenAiResponsesChatProvider(metering(), stores, fsService, { - apiKey: 'k', - }), + new OpenAiResponsesChatProvider({ apiKey: 'k' }, metering()), }, { name: 'openrouter', diff --git a/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.test.ts b/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.test.ts index cc7e1686a..3392d7385 100644 --- a/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/together/TogetherAIProvider.test.ts @@ -51,6 +51,7 @@ import { setupTestServer } from '../../../../testUtil.js'; import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { TogetherAIProvider } from './TogetherAIProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── Together SDK mock ─────────────────────────────────────────────── @@ -188,7 +189,7 @@ describe('TogetherAIProvider model catalog', () => { it('list() flattens canonical ids and aliases for chat models only', async () => { const { provider } = makeProvider(); - const ids = await provider.list(); + const ids = modelLookupNames(await provider.models()); // Embedding-typed models are filtered out. expect(ids).not.toContain('togetherai:some/embedding-model'); // Canonical id is prefixed with togetherai: diff --git a/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.test.ts b/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.test.ts index dc28c1238..1fa0b6a4d 100644 --- a/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/zai/ZAIProvider.test.ts @@ -50,6 +50,7 @@ import { withTestActor } from '../../../integrationTestUtil.js'; import { AIChatStream } from '../../utils/Streaming.js'; import { ZAI_MODELS } from './models.js'; import { ZAIProvider } from './ZAIProvider.js'; +import { modelLookupNames } from '../../utils/modelRouting.js'; // ── OpenAI SDK mock ───────────────────────────────────────────────── // @@ -186,7 +187,7 @@ describe('ZAIProvider model catalog', () => { it('list() flattens canonical ids and aliases', () => { const { provider } = makeProvider(); - const names = provider.list(); + const names = modelLookupNames(provider.models()); for (const m of ZAI_MODELS) { expect(names).toContain(m.id); for (const a of m.aliases ?? []) { diff --git a/src/backend/drivers/ai-chat/types.ts b/src/backend/drivers/ai-chat/types.ts index d8bf7d80e..c1e97df64 100644 --- a/src/backend/drivers/ai-chat/types.ts +++ b/src/backend/drivers/ai-chat/types.ts @@ -291,7 +291,6 @@ export type IChatCompleteResult = IChatStreamResult | IChatMessageResult; export interface IChatProvider { models(extra_params?: unknown): IChatModel[] | Promise; - list(): string[] | Promise; getDefaultModel(): string; /** * `model` is this provider's own catalog entry for `arg.model`, as the diff --git a/src/backend/drivers/ai-chat/utils/OpenAIUtil.js b/src/backend/drivers/ai-chat/utils/OpenAIUtil.js index fbdc34276..0d14eb726 100644 --- a/src/backend/drivers/ai-chat/utils/OpenAIUtil.js +++ b/src/backend/drivers/ai-chat/utils/OpenAIUtil.js @@ -148,6 +148,38 @@ export const toOpenAIChatMessages = ( return out; }; +/** + * Moves a message's `tool_use` blocks into OpenAI `tool_calls`, in place. True + * when this call created `tool_calls`. `canonicalId` keeps the Responses item + * id a replayed call carries. + */ +const hoistToolUseBlocks = (msg, { canonicalId = false } = {}) => { + const content = msg.content; + let hoisted = false; + for (let i = content.length - 1; i >= 0; i--) { + const block = content[i]; + if (block.type !== 'tool_use') continue; + if (!msg.tool_calls) { + msg.tool_calls = []; + hoisted = true; + } + msg.tool_calls.push({ + id: block.id, + ...(canonicalId ? { canonical_id: block.canonical_id } : {}), + type: 'function', + function: { + name: block.name, + arguments: JSON.stringify(block.input), + }, + ...(block.extra_content + ? { extra_content: block.extra_content } + : {}), + }); + content.splice(i, 1); + } + return hoisted; +}; + /** * Process input messages from Puter's normalized format to OpenAI's format May * make changes in-place. @@ -171,32 +203,7 @@ export const process_input_messages = async (messages) => { } } - // coerce tool calls - let is_tool_call = false; - for (let i = content.length - 1; i >= 0; i--) { - const content_block = content[i]; - - if (content_block.type === 'tool_use') { - if (!msg.tool_calls) { - msg.tool_calls = []; - is_tool_call = true; - } - msg.tool_calls.push({ - id: content_block.id, - type: 'function', - function: { - name: content_block.name, - arguments: JSON.stringify(content_block.input), - }, - ...(content_block.extra_content - ? { extra_content: content_block.extra_content } - : {}), - }); - content.splice(i, 1); - } - } - - if (is_tool_call) msg.content = null; + if (hoistToolUseBlocks(msg)) msg.content = null; // coerce tool results // (we assume multiple tool results were already split into separate messages) @@ -380,10 +387,7 @@ export const process_input_messages_responses_api = async (messages) => { } } - // coerce tool calls - let is_tool_call = false; - for (let i = content.length - 1; i >= 0; i--) { - const content_block = content[i]; + for (const content_block of content) { if ( content_block.type === 'text' && (msg.role === 'user' || msg.role === 'system') @@ -393,30 +397,9 @@ export const process_input_messages_responses_api = async (messages) => { if (content_block.type === 'text' && msg.role === 'assistant') { content_block.type = 'output_text'; } - - if (content_block.type === 'tool_use') { - if (!msg.tool_calls) { - msg.tool_calls = []; - is_tool_call = true; - } - msg.tool_calls.push({ - id: content_block.id, - canonical_id: content_block.canonical_id, - type: 'function', - function: { - name: content_block.name, - arguments: JSON.stringify(content_block.input), - }, - ...(content_block.extra_content - ? { extra_content: content_block.extra_content } - : {}), - }); - - content.splice(i, 1); - } } - if (is_tool_call) { + if (hoistToolUseBlocks(msg, { canonicalId: true })) { // One Responses `function_call` item per tool_use block. A // parallel tool-call turn used to keep only the first, which // silently dropped every other call the model made — the @@ -457,28 +440,6 @@ export const process_input_messages_responses_api = async (messages) => { return flattened; }; -export const create_usage_calculator = ({ model_details }) => { - return ({ usage }) => { - const tokens = []; - - tokens.push({ - type: 'prompt', - model: model_details.id, - amount: usage.prompt_tokens, - cost: model_details.cost.input * usage.prompt_tokens, - }); - - tokens.push({ - type: 'completion', - model: model_details.id, - amount: usage.completion_tokens, - cost: model_details.cost.output * usage.completion_tokens, - }); - - return tokens; - }; -}; - export const extractMeteredUsage = (usage) => { return { prompt_tokens: usage.prompt_tokens ?? 0, @@ -715,24 +676,10 @@ export const create_chat_stream_handler = }; export const create_chat_stream_handler_responses_api = - ({ deviations, completion, usage_calculator }) => + ({ completion, usage_calculator }) => async ({ chatStream }) => { - deviations = Object.assign( - { - // affected by: Groq - index_usage_from_stream_chunk: (chunk) => chunk.usage, - // affected by: Mistral - chunk_but_like_actually: (chunk) => chunk, - index_tool_calls_from_stream_choice: (choice) => - choice.delta.tool_calls, - }, - deviations, - ); - const message = chatStream.message(); const textblock = message.contentBlock({ type: 'text' }); - let toolblock = null; - const mode = 'text'; let last_usage = null; let completed = false; @@ -844,7 +791,7 @@ export const create_chat_stream_handler_responses_api = ) { sawFunctionCall = true; const tool_call = chunk.item; - toolblock = message.contentBlock({ + const toolblock = message.contentBlock({ type: 'tool_use', canonical_id: tool_call.id, id: tool_call.call_id, @@ -881,8 +828,7 @@ export const create_chat_stream_handler_responses_api = const usageDetails = usageDetailsFromTrackedUsage(usage); if (usageDetails) chatStream.setUsageDetails(usageDetails); - if (mode === 'text') textblock.end(); - if (mode === 'tool') toolblock.end(); + textblock.end(); message.end(); chatStream.end(usage); @@ -985,7 +931,6 @@ export const handle_completion_output = async ( /** * @param {object} params - * @param {Record} [params.deviations] * @param {boolean} [params.stream] * @param {any} params.completion * @param {((text: string) => Promise<{ flagged: boolean }>) | undefined} [params.moderate] @@ -999,24 +944,14 @@ export const handle_completion_output = async ( * @returns {ReturnType} */ export const handle_completion_output_responses_api = async ({ - deviations, stream, completion, moderate, usage_calculator, finally_fn, }) => { - deviations = Object.assign( - { - // affected by: Mistral - coerce_completion_usage: (completion) => completion.usage, - }, - deviations, - ); - if (stream) { const init_chat_stream = create_chat_stream_handler_responses_api({ - deviations, completion, usage_calculator, }); diff --git a/src/backend/drivers/ai-chat/utils/OpenAIUtil.test.ts b/src/backend/drivers/ai-chat/utils/OpenAIUtil.test.ts index 3a810529d..637183f18 100644 --- a/src/backend/drivers/ai-chat/utils/OpenAIUtil.test.ts +++ b/src/backend/drivers/ai-chat/utils/OpenAIUtil.test.ts @@ -25,7 +25,6 @@ import { withTestActor } from '../../integrationTestUtil.js'; import { create_chat_stream_handler, create_chat_stream_handler_responses_api, - create_usage_calculator, extractMeteredUsage, handle_completion_output, handle_completion_output_responses_api, @@ -778,36 +777,6 @@ describe('process_input_messages_responses_api', () => { }); }); -// ── create_usage_calculator ───────────────────────────────────────── - -describe('create_usage_calculator', () => { - it('emits prompt + completion token rows priced by model_details.cost', () => { - const calc = create_usage_calculator({ - model_details: { - id: 'gpt-test', - cost: { input: 0.01, output: 0.02 }, - }, - }); - const tokens = calc({ - usage: { prompt_tokens: 100, completion_tokens: 50 }, - }); - expect(tokens).toEqual([ - { - type: 'prompt', - model: 'gpt-test', - amount: 100, - cost: 100 * 0.01, - }, - { - type: 'completion', - model: 'gpt-test', - amount: 50, - cost: 50 * 0.02, - }, - ]); - }); -}); - // ── extractMeteredUsage ───────────────────────────────────────────── describe('extractMeteredUsage', () => {