mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-25 08:09:15 +00:00
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.
This commit is contained in:
1 parent
ddff917630
commit
c6352cf71f
34 files changed
+108
-384
No files matched your search
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<IChatModel[]> {
|
||||
return [];
|
||||
}
|
||||
list(): string[] | Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
async complete(_arg: ICompleteArguments): Promise<IChatCompleteResult> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,7 @@ export class FakeChatProvider implements IChatProvider {
|
||||
},
|
||||
];
|
||||
}
|
||||
async list() {
|
||||
return ['fake', 'costly', 'abuse'];
|
||||
}
|
||||
|
||||
async complete({
|
||||
messages,
|
||||
stream,
|
||||
|
||||
@@ -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<string[]> {
|
||||
const models = this.models();
|
||||
return Array.isArray(models)
|
||||
? modelLookupNames(models)
|
||||
: this.#listAsync(models);
|
||||
}
|
||||
|
||||
async #listAsync(models: Promise<IChatModel[]>) {
|
||||
return modelLookupNames(await models);
|
||||
}
|
||||
|
||||
/** The model key this provider records usage under. */
|
||||
meteringModelKey(modelId: string): string {
|
||||
const prefix = this.#options.meteringPrefix;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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 = <T>(items: T[]): AsyncIterable<T> => ({
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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<string, unknown> =>
|
||||
* 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ?? []) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 ?? []) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
+1
-3
@@ -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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ?? []) {
|
||||
|
||||
@@ -291,7 +291,6 @@ export type IChatCompleteResult = IChatStreamResult | IChatMessageResult;
|
||||
|
||||
export interface IChatProvider {
|
||||
models(extra_params?: unknown): IChatModel[] | Promise<IChatModel[]>;
|
||||
list(): string[] | Promise<string[]>;
|
||||
getDefaultModel(): string;
|
||||
/**
|
||||
* `model` is this provider's own catalog entry for `arg.model`, as the
|
||||
|
||||
@@ -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<string, unknown>} [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<import('../types').IChatProvider['complete']>}
|
||||
*/
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in new issue
Block a user