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', () => {