blob: 94f3edac23084e268e69ca218a2a90f1d86c0a71 [file]
import {
PROVIDER_DEFAULTS,
effectiveBaseUrl,
providerAuthSupportsApiKey,
type LlmConnection,
type ModelInfo,
} from '@maka/core';
import { generalizedErrorMessage } from '@maka/core';
import {
CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION,
CONNECTION_MODEL_ID_MAX_LENGTH,
normalizeConnectionModelDiscoveryResult,
} from '@maka/core';
import { anthropicV1Url, googleApiUrl } from './provider-urls.js';
import { claudeSubscriptionHeaders, openAiCodexHeaders } from './subscription-auth.js';
import {
GITHUB_COPILOT_API_VERSION,
GITHUB_COPILOT_COMPAT_HEADERS,
} from './subscription-credentials.js';
import {
ConnectionEffectFetchError,
fetchForConnectionEffect,
type ConnectionEffectFetch,
type ConnectionEffectFetchDependency,
type ConnectionEffectFetchOptions,
type ConnectionEffectResponse,
} from './connection-effect-fetch.js';
import {
ConnectionEffectHttpError,
ConnectionEffectInvalidResponseError,
classifyConnectionEffectStatus,
type ConnectionEffectConnection,
type ConnectionEffectError,
type ConnectionModelDiscoveryEffectOutcome,
} from './connection-effect-outcome.js';
const MODEL_FETCH_TIMEOUT_MS = 10_000;
const CLOUDFLARE_MODEL_PAGE_SIZE = 50;
const CLOUDFLARE_MODEL_MAX_REQUEST_PAGES =
Math.ceil(CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION / CLOUDFLARE_MODEL_PAGE_SIZE) + 1;
const COHERE_MODEL_PAGE_SIZE = 1_000;
const COHERE_MODEL_MAX_REQUEST_PAGES =
Math.ceil(CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION / COHERE_MODEL_PAGE_SIZE) + 1;
const FIREWORKS_PAGE_SIZE = 200;
const FIREWORKS_MAX_REQUEST_PAGES =
Math.ceil(CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION / FIREWORKS_PAGE_SIZE) + 1;
const FIREWORKS_MAX_ACCOUNTS = 32;
const FIREWORKS_ACCOUNT_CONCURRENCY = 4;
const PROVIDER_PAGE_TOKEN_MAX_LENGTH = 2_048;
type RawProviderModel = {
id?: string;
name?: string;
display_name?: string;
type?: string;
tags?: string[];
input_modalities?: string[];
output_modalities?: string[];
capabilities?: { reasoning?: boolean };
supports_image_in?: boolean;
supports_reasoning?: boolean;
context_length?: number;
context_window?: number;
max_tokens?: number;
providers?: Array<{
status?: string;
supports_tools?: boolean;
}>;
};
type RawFireworksModel = {
name?: string;
displayName?: string;
contextLength?: number;
supportsImageInput?: boolean;
supportsTools?: boolean;
};
type RawCohereModel = {
name?: string;
is_deprecated?: boolean;
endpoints?: string[];
context_length?: number;
};
type RawCloudflareModel = {
name?: unknown;
};
type RawGitHubCopilotModel = {
id?: string;
name?: string;
model_picker_enabled?: boolean;
supported_endpoints?: string[];
policy?: { state?: string };
capabilities?: {
limits?: {
max_context_window_tokens?: number;
max_prompt_tokens?: number;
max_output_tokens?: number;
vision?: { supported_media_types?: string[] };
};
supports?: {
adaptive_thinking?: boolean;
max_thinking_budget?: number;
min_thinking_budget?: number;
reasoning_effort?: string[];
tool_calls?: boolean;
vision?: boolean;
};
};
};
type FireworksModelDiscovery = Extract<
(typeof PROVIDER_DEFAULTS)[keyof typeof PROVIDER_DEFAULTS]['modelDiscovery'],
{ kind: 'fireworks' }
>;
export async function fetchProviderModels(
connection: LlmConnection,
apiKey: string,
options: ConnectionEffectFetchOptions = {},
): Promise<ModelInfo[]> {
try {
return normalizeDiscoveredModels(
await fetchProviderModelsStrict(connection, apiKey, options.fetch),
);
} catch (error) {
// Preserve status-bearing discovery errors so the sync layer can classify
// auth/protocol/network failures; only wrap unknown errors for display.
if (
error instanceof OpenAiCodexDiscoveryError ||
error instanceof ProviderModelDiscoveryHttpError
) {
throw error;
}
throw new Error(generalizedErrorMessage(error, 'Failed to fetch provider models'));
}
}
export async function runConnectionModelDiscoveryEffect(
connection: ConnectionEffectConnection,
apiKey: string,
options: ConnectionEffectFetchDependency,
): Promise<ConnectionModelDiscoveryEffectOutcome> {
try {
return {
ok: true,
models: normalizeConnectionEffectModels(
await fetchProviderModelsStrict(connection, apiKey, options.fetch),
),
};
} catch (error) {
return { ok: false, error: classifyDiscoveryError(error) };
}
}
async function fetchProviderModelsStrict(
connection: ConnectionEffectConnection,
apiKey: string,
fetchFn: ConnectionEffectFetch | undefined,
): Promise<ModelInfo[]> {
const baseUrl = effectiveBaseUrl(connection);
const definition = PROVIDER_DEFAULTS[connection.providerType];
// Unknown providerType → no discovery path. Throw a clear error (caught and
// generalized by the caller) rather than crashing on `.modelDiscovery`.
// Mirrors `isFakeBackend` in @maka/core/connection-readiness.ts.
if (!definition) {
throw new Error(`Unknown provider type "${connection.providerType}"`);
}
const discovery = definition.modelDiscovery;
if (discovery.kind === 'fallback') {
return definition.fallbackModels.map((id) => ({ id }));
}
if (discovery.kind === 'ollama') {
const r = await fetchForConnectionEffect(fetchFn, `${ollamaRoot(baseUrl)}/api/tags`, {
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
});
if (!r.ok) {
await r.cancel();
throw new ConnectionEffectHttpError(r.status);
}
const data = await readProviderJson<{ models?: unknown }>(r);
return providerObjectArray<{ name?: string }>(data.models, 'Ollama models').flatMap((model) =>
model.name ? [{ id: model.name }] : [],
);
}
if (discovery.kind === 'fireworks') {
return fetchFireworksModels(baseUrl, apiKey, discovery, fetchFn);
}
if (discovery.kind === 'cohere') {
return fetchCohereModels(baseUrl, apiKey, fetchFn);
}
if (discovery.kind === 'cloudflare') {
return fetchCloudflareModels(baseUrl, apiKey, fetchFn);
}
if (discovery.auth === 'github-copilot') {
return fetchGitHubCopilotModels(baseUrl, apiKey, fetchFn);
}
if (discovery.auth === 'openai-codex') {
return fetchOpenAiCodexModels(baseUrl, apiKey, fetchFn);
}
switch (definition.protocol) {
case 'anthropic': {
const r = await fetchForConnectionEffect(fetchFn, anthropicV1Url(baseUrl, '/models'), {
headers: anthropicModelHeaders(
discovery.auth === 'claude-subscription' ? discovery.auth : undefined,
apiKey,
),
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
});
if (!r.ok) {
await r.cancel();
throw new ConnectionEffectHttpError(r.status);
}
const data = await readProviderJson<{ data?: unknown }>(r);
const models = providerObjectArray<RawProviderModel>(data.data, 'Anthropic models')
.map(toModelInfo)
.filter((model): model is ModelInfo => model !== null);
return filterDiscoveredModels(models, discovery.filter, definition.fallbackModels);
}
case 'openai': {
const r = await fetchForConnectionEffect(
fetchFn,
modelListUrl(baseUrl, discovery.path, discovery.query),
{
headers: {
'content-type': 'application/json',
...(apiKey &&
(discovery.auth === 'oauth-bearer' ||
(discovery.auth !== 'none' && providerAuthSupportsApiKey(connection.providerType)))
? { authorization: `Bearer ${apiKey}` }
: {}),
},
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
},
);
if (!r.ok) {
await r.cancel();
if (connection.providerType === 'xai-oauth') {
throw new ProviderModelDiscoveryHttpError(r.status);
}
throw new ConnectionEffectHttpError(r.status);
}
const data = await readProviderJson<{ data?: unknown } | unknown[]>(r);
const rawModels =
discovery.responseShape === 'array-or-data'
? Array.isArray(data)
? providerObjectArray<RawProviderModel>(data, 'provider models')
: providerObjectArray<RawProviderModel>(data.data, 'provider models')
: Array.isArray(data)
? []
: providerObjectArray<RawProviderModel>(data.data, 'provider models');
const models = rawModels
.filter((model) => discovery.filter !== 'language-models' || model.type === 'language')
.map(toModelInfo)
.filter((model): model is ModelInfo => model !== null);
return filterDiscoveredModels(models, discovery.filter, definition.fallbackModels);
}
case 'google': {
const r = await fetchForConnectionEffect(fetchFn, googleApiUrl(baseUrl, '/models', apiKey), {
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
});
if (!r.ok) {
await r.cancel();
throw new ConnectionEffectHttpError(r.status);
}
const data = await readProviderJson<{ models?: unknown }>(r);
return providerObjectArray<{ name?: string }>(data.models, 'Google models').flatMap(
(model) => {
const id = model.name?.split('/').pop();
return id ? [{ id }] : [];
},
);
}
case 'cohere':
throw new Error('Cohere requires native model discovery');
}
}
async function fetchCloudflareModels(
baseUrl: string,
apiKey: string,
fetchFn: ConnectionEffectFetch | undefined,
): Promise<ModelInfo[]> {
const models: ModelInfo[] = [];
let page = 1;
let rawModelCount = 0;
const signal = AbortSignal.timeout(MODEL_FETCH_TIMEOUT_MS);
while (page <= CLOUDFLARE_MODEL_MAX_REQUEST_PAGES) {
const url = cloudflareModelsUrl(baseUrl, page);
const response = await fetchForConnectionEffect(fetchFn, url, {
headers: { authorization: `Bearer ${apiKey}` },
signal,
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
});
if (!response.ok) {
await response.cancel();
throw new ConnectionEffectHttpError(response.status);
}
const data = await readProviderJson<{
success?: unknown;
result?: unknown;
}>(response);
if (data.success !== true) {
throw new ConnectionEffectInvalidResponseError('Invalid Cloudflare models response');
}
const rawModels = providerObjectArray<RawCloudflareModel>(
data.result,
'Cloudflare models',
true,
);
rawModelCount += rawModels.length;
if (rawModelCount > CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many models');
}
models.push(
...rawModels.flatMap((model) => (typeof model.name === 'string' ? [{ id: model.name }] : [])),
);
if (rawModels.length === 0) return models;
page += 1;
}
throw new ConnectionEffectInvalidResponseError('Provider returned too many models');
}
function cloudflareModelsUrl(baseUrl: string, page: number): string {
const url = new URL(baseUrl);
if (!/\/ai\/v1\/?$/.test(url.pathname)) {
throw new Error('Cloudflare Workers AI base URL must end with /ai/v1');
}
url.pathname = url.pathname.replace(/\/ai\/v1\/?$/, '/ai/models/search');
url.search = new URLSearchParams({
page: String(page),
per_page: String(CLOUDFLARE_MODEL_PAGE_SIZE),
task: 'Text Generation',
}).toString();
return url.toString();
}
function normalizeDiscoveredModels(models: ModelInfo[]): ModelInfo[] {
const unique = new Map<string, ModelInfo>();
for (const model of models) {
if (typeof model?.id !== 'string') continue;
const id = model.id.trim();
if (
!id ||
id.length > CONNECTION_MODEL_ID_MAX_LENGTH ||
/[\u0000-\u001f\u007f]/.test(id) ||
unique.has(id)
) {
continue;
}
unique.set(id, { ...model, id });
if (unique.size > CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many models');
}
}
return [...unique.values()];
}
function normalizeConnectionEffectModels(models: ModelInfo[]): readonly ModelInfo[] {
try {
return normalizeConnectionModelDiscoveryResult({
models: normalizeDiscoveredModels(models),
source: 'fetched',
fetchedAt: 0,
}).models;
} catch (error) {
throw new ConnectionEffectInvalidResponseError('Provider returned invalid model metadata', {
cause: error,
});
}
}
export async function fetchGitHubCopilotModels(
baseUrl: string,
accessToken: string,
fetchFn?: ConnectionEffectFetch,
): Promise<ModelInfo[]> {
const response = await fetchForConnectionEffect(fetchFn, `${stripTrailing(baseUrl)}/models`, {
headers: {
Authorization: `Bearer ${accessToken}`,
...GITHUB_COPILOT_COMPAT_HEADERS,
'Openai-Intent': 'conversation-edits',
'X-GitHub-Api-Version': GITHUB_COPILOT_API_VERSION,
},
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
});
if (!response.ok) {
await response.cancel();
throw new ConnectionEffectHttpError(response.status);
}
const payload = await readProviderJson<{ data?: unknown }>(response);
return providerObjectArray<RawGitHubCopilotModel>(
payload.data,
'GitHub Copilot models',
true,
).flatMap(toGitHubCopilotModelInfo);
}
type RawOpenAiCodexModel = {
slug?: unknown;
visibility?: unknown;
priority?: unknown;
context_window?: unknown;
};
/**
* Discovery error carrying the HTTP status, so callers (syncOpenAiCodexConnection)
* can classify auth failures (401/403) vs protocol errors (4xx) vs transient
* network failures without string-matching the message.
*/
export class OpenAiCodexDiscoveryError extends ConnectionEffectHttpError {
constructor(status: number) {
super(status);
this.name = 'OpenAiCodexDiscoveryError';
}
}
/** Structured status for standard provider `/models` endpoints. */
export class ProviderModelDiscoveryHttpError extends ConnectionEffectHttpError {
constructor(status: number) {
super(status);
this.name = 'ProviderModelDiscoveryHttpError';
}
}
/**
* Discover models from the ChatGPT/Codex OAuth backend
* (`chatgpt.com/backend-api/codex/models`). Unlike the public OpenAI API
* `/v1/models`, this endpoint reports the slugs the signed-in ChatGPT account
* can actually use over the Codex backend, including OAuth-only slugs such
* as `gpt-5.3-codex-spark`. Entries with `visibility: hide|hidden` are
* dropped; the rest are sorted by `priority` (ascending) to match the
* ChatGPT/Codex picker order.
*/
export async function fetchOpenAiCodexModels(
baseUrl: string,
accessToken: string,
fetchFn?: ConnectionEffectFetch,
): Promise<ModelInfo[]> {
const response = await fetchForConnectionEffect(
fetchFn,
`${stripTrailing(baseUrl)}/models?client_version=1.0.0`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
...openAiCodexHeaders(accessToken),
'content-type': 'application/json',
},
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
},
);
if (!response.ok) {
await response.cancel();
throw new OpenAiCodexDiscoveryError(response.status);
}
const payload = await readProviderJson<{ models?: unknown }>(response);
const models = providerObjectArray<RawOpenAiCodexModel>(
payload.models,
'OpenAI Codex models',
true,
);
const visible = models.filter((model) => {
if (!model || typeof model.slug !== 'string' || !model.slug.trim()) return false;
const visibility =
typeof model.visibility === 'string' ? model.visibility.trim().toLowerCase() : '';
return visibility !== 'hide' && visibility !== 'hidden';
});
visible.sort((a, b) => priorityOfOpenAiCodexModel(a) - priorityOfOpenAiCodexModel(b));
return visible.map((model) => {
const entry: ModelInfo = { id: (model.slug as string).trim() };
const contextWindow = contextWindowOfOpenAiCodexModel(model);
if (contextWindow !== undefined) entry.contextWindow = contextWindow;
return entry;
});
}
function priorityOfOpenAiCodexModel(model: RawOpenAiCodexModel): number {
return typeof model.priority === 'number' && Number.isFinite(model.priority)
? model.priority
: 10_000;
}
function contextWindowOfOpenAiCodexModel(model: RawOpenAiCodexModel): number | undefined {
return typeof model.context_window === 'number' &&
Number.isFinite(model.context_window) &&
model.context_window > 0
? model.context_window
: undefined;
}
function toGitHubCopilotModelInfo(model: RawGitHubCopilotModel): ModelInfo[] {
if (
typeof model.id !== 'string' ||
!model.id ||
model.model_picker_enabled !== true ||
model.policy?.state === 'disabled' ||
model.capabilities?.supports?.tool_calls !== true
)
return [];
assertOptionalArray(model.supported_endpoints, 'model supported_endpoints');
assertOptionalArray(
model.capabilities.supports.reasoning_effort,
'model capabilities.supports.reasoning_effort',
);
assertOptionalArray(
model.capabilities.limits?.vision?.supported_media_types,
'model capabilities.limits.vision.supported_media_types',
);
const endpoints = model.supported_endpoints ?? [];
const apiProtocol = endpoints.includes('/v1/messages')
? ('anthropic-messages' as const)
: endpoints.includes('/responses')
? ('openai-responses' as const)
: endpoints.includes('/chat/completions')
? ('openai-chat' as const)
: null;
if (!apiProtocol) return [];
const limits = model.capabilities.limits;
const supports = model.capabilities.supports;
const reasoning =
supports.adaptive_thinking === true ||
(supports.reasoning_effort?.length ?? 0) > 0 ||
supports.max_thinking_budget !== undefined ||
supports.min_thinking_budget !== undefined;
const vision =
supports.vision === true ||
limits?.vision?.supported_media_types?.some(
(type) => typeof type === 'string' && type.startsWith('image/'),
) === true;
const contextWindow = limits?.max_context_window_tokens ?? limits?.max_prompt_tokens;
return [
{
id: model.id,
...(model.name ? { displayName: model.name } : {}),
...(typeof contextWindow === 'number' ? { contextWindow } : {}),
...(typeof limits?.max_output_tokens === 'number'
? { maxOutputTokens: limits.max_output_tokens }
: {}),
apiProtocol,
capabilities: { vision, reasoning, functionCalling: true },
},
];
}
async function fetchCohereModels(
baseUrl: string,
apiKey: string,
fetchFn: ConnectionEffectFetch | undefined,
): Promise<ModelInfo[]> {
const root = stripTrailing(baseUrl).replace(/\/v2$/, '');
const models: ModelInfo[] = [];
const seenPageTokens = new Set<string>();
const signal = AbortSignal.timeout(MODEL_FETCH_TIMEOUT_MS);
let pageToken: string | undefined;
let pageCount = 0;
let rawModelCount = 0;
while (true) {
pageCount += 1;
if (pageCount > COHERE_MODEL_MAX_REQUEST_PAGES) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many model pages');
}
const query = new URLSearchParams({
endpoint: 'chat',
page_size: String(COHERE_MODEL_PAGE_SIZE),
});
if (pageToken) query.set('page_token', pageToken);
const response = await fetchForConnectionEffect(
fetchFn,
`${root}/v1/models?${query.toString()}`,
{
headers: { authorization: `Bearer ${apiKey}` },
signal,
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
},
);
if (!response.ok) {
await response.cancel();
throw new ConnectionEffectHttpError(response.status);
}
const data = await readProviderJson<{ models?: unknown; next_page_token?: unknown }>(response);
const rawModels = providerObjectArray<RawCohereModel>(data.models, 'Cohere models');
rawModelCount += rawModels.length;
if (rawModelCount > CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many models');
}
models.push(
...rawModels.flatMap((model) => {
if (typeof model.name !== 'string' || !model.name || model.is_deprecated === true) {
return [];
}
assertOptionalArray(model.endpoints, 'model endpoints');
if (!model.endpoints?.includes('chat')) return [];
return [
{
id: model.name,
...(typeof model.context_length === 'number'
? { contextWindow: model.context_length }
: {}),
},
];
}),
);
pageToken = nextProviderPageToken(data.next_page_token);
if (!pageToken) return models;
if (seenPageTokens.has(pageToken)) {
throw new ConnectionEffectInvalidResponseError('Provider repeated a model page token');
}
seenPageTokens.add(pageToken);
}
}
function filterDiscoveredModels(
models: ModelInfo[],
filter: 'fallback-models' | 'language-models' | 'tool-capable' | undefined,
fallbackModels: readonly string[],
): ModelInfo[] {
if (filter === 'tool-capable') {
return models.filter((model) => model.capabilities?.functionCalling === true);
}
if (filter !== 'fallback-models') return models;
const supported = new Set(fallbackModels);
return models.filter((model) => supported.has(model.id));
}
function modelListUrl(
baseUrl: string,
path: string | undefined,
query: Readonly<Record<string, string>> | undefined,
): string {
const url = path
? new URL(path, `${stripTrailing(baseUrl)}/`).toString()
: `${stripTrailing(baseUrl)}/models`;
const search = query ? new URLSearchParams(query).toString() : '';
return search ? `${url}?${search}` : url;
}
async function fetchFireworksModels(
baseUrl: string,
apiKey: string,
discovery: FireworksModelDiscovery,
fetchFn: ConnectionEffectFetch | undefined,
): Promise<ModelInfo[]> {
const root = stripTrailing(baseUrl).replace(/\/inference\/v1$/, '');
const headers = {
'content-type': 'application/json',
authorization: `Bearer ${apiKey}`,
};
const signal = AbortSignal.timeout(MODEL_FETCH_TIMEOUT_MS);
const fetchPages = async <T extends object>(
path: string,
query: Readonly<Record<string, string>>,
itemKey: 'accounts' | 'models',
maxItems: number,
reserveItems?: (count: number) => void,
): Promise<T[]> => {
const items: T[] = [];
const seenPageTokens = new Set<string>();
let pageToken: string | undefined;
let pageCount = 0;
while (true) {
pageCount += 1;
if (pageCount > FIREWORKS_MAX_REQUEST_PAGES) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many model pages');
}
const search = new URLSearchParams(query);
if (pageToken) search.set('pageToken', pageToken);
const response = await fetchForConnectionEffect(
fetchFn,
`${root}${path}?${search.toString()}`,
{
headers,
signal,
timeoutMs: MODEL_FETCH_TIMEOUT_MS,
},
);
if (!response.ok) {
await response.cancel();
throw new ConnectionEffectHttpError(response.status);
}
const data = await readProviderJson<{
accounts?: unknown;
models?: unknown;
nextPageToken?: unknown;
}>(response);
const rawItems = providerObjectArray<T>(data[itemKey], `Fireworks ${itemKey}`);
reserveItems?.(rawItems.length);
items.push(...rawItems);
if (items.length > maxItems) {
throw new ConnectionEffectInvalidResponseError(`Provider returned too many ${itemKey}`);
}
pageToken = nextProviderPageToken(data.nextPageToken);
if (!pageToken) return items;
if (seenPageTokens.has(pageToken)) {
throw new ConnectionEffectInvalidResponseError('Provider repeated a model page token');
}
seenPageTokens.add(pageToken);
}
};
const accounts = await fetchPages<{ name?: string }>(
discovery.accountsPath,
{ pageSize: String(FIREWORKS_PAGE_SIZE) },
'accounts',
FIREWORKS_MAX_ACCOUNTS,
);
const accountNames = [
...accounts.flatMap((account) =>
account.name && /^accounts\/[^/]+$/.test(account.name) ? [account.name] : [],
),
discovery.publicAccount,
].filter((name, index, names) => names.indexOf(name) === index);
if (accountNames.length > FIREWORKS_MAX_ACCOUNTS) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many accounts');
}
let rawModelCount = 0;
const reserveModels = (count: number) => {
rawModelCount += count;
if (rawModelCount > CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION) {
throw new ConnectionEffectInvalidResponseError('Provider returned too many models');
}
};
const modelLists: RawFireworksModel[][] = [];
for (let index = 0; index < accountNames.length; index += FIREWORKS_ACCOUNT_CONCURRENCY) {
modelLists.push(
...(await Promise.all(
accountNames
.slice(index, index + FIREWORKS_ACCOUNT_CONCURRENCY)
.map((accountName) =>
fetchPages<RawFireworksModel>(
`/v1/${accountName}/models`,
discovery.query,
'models',
CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION,
reserveModels,
),
),
)),
);
}
return modelLists.flat().flatMap((model) => {
if (!model.name) return [];
const capabilities: NonNullable<ModelInfo['capabilities']> = {};
if (typeof model.supportsImageInput === 'boolean')
capabilities.vision = model.supportsImageInput;
if (typeof model.supportsTools === 'boolean')
capabilities.functionCalling = model.supportsTools;
return [
{
id: model.name,
...(model.displayName ? { displayName: model.displayName } : {}),
...(typeof model.contextLength === 'number' ? { contextWindow: model.contextLength } : {}),
...(Object.keys(capabilities).length ? { capabilities } : {}),
},
];
});
}
function toModelInfo(model: RawProviderModel): ModelInfo | null {
if (typeof model.id !== 'string' || !model.id) return null;
assertOptionalArray(model.input_modalities, 'model input_modalities');
assertOptionalArray(model.output_modalities, 'model output_modalities');
assertOptionalArray(model.tags, 'model tags');
const providers = providerObjectArray<NonNullable<RawProviderModel['providers']>[number]>(
model.providers,
'model providers',
);
const contextWindow = model.context_length ?? model.context_window;
const capabilities: NonNullable<ModelInfo['capabilities']> = {};
if (model.input_modalities?.includes('image')) capabilities.vision = true;
if (typeof model.capabilities?.reasoning === 'boolean')
capabilities.reasoning = model.capabilities.reasoning;
if (typeof model.supports_image_in === 'boolean') capabilities.vision = model.supports_image_in;
if (typeof model.supports_reasoning === 'boolean')
capabilities.reasoning = model.supports_reasoning;
if (model.tags?.includes('vision')) capabilities.vision = true;
if (model.tags?.includes('reasoning')) capabilities.reasoning = true;
if (model.tags?.includes('tool-use')) capabilities.functionCalling = true;
if (model.providers) {
capabilities.functionCalling = providers.some(
(provider) => provider.status === 'live' && provider.supports_tools === true,
);
}
return {
id: model.id,
...(model.display_name || model.name ? { displayName: model.display_name ?? model.name } : {}),
...(typeof contextWindow === 'number' ? { contextWindow } : {}),
...(typeof model.max_tokens === 'number' ? { maxOutputTokens: model.max_tokens } : {}),
...(Object.keys(capabilities).length ? { capabilities } : {}),
};
}
function anthropicModelHeaders(
auth: 'claude-subscription' | undefined,
apiKey: string,
): Record<string, string> {
if (auth === 'claude-subscription') {
return {
...claudeSubscriptionHeaders(),
Authorization: `Bearer ${apiKey}`,
'anthropic-version': '2023-06-01',
};
}
return {
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
};
}
function stripTrailing(u: string): string {
return u.replace(/\/+$/, '');
}
function ollamaRoot(baseUrl: string): string {
return stripTrailing(baseUrl).replace(/\/v1$/, '');
}
async function readProviderJson<T>(response: ConnectionEffectResponse): Promise<T> {
const value = await response.readJson<unknown>();
if (value === null || typeof value !== 'object') {
throw new ConnectionEffectInvalidResponseError('Invalid provider JSON response structure');
}
return value as T;
}
function providerObjectArray<T extends object>(
value: unknown,
label: string,
required = false,
): T[] {
if (value === undefined && !required) return [];
if (
!Array.isArray(value) ||
value.some((item) => item === null || typeof item !== 'object' || Array.isArray(item))
) {
throw new ConnectionEffectInvalidResponseError(`Invalid ${label} response`);
}
return value as T[];
}
function assertOptionalArray(
value: unknown,
label: string,
): asserts value is unknown[] | undefined {
if (value !== undefined && !Array.isArray(value)) {
throw new ConnectionEffectInvalidResponseError(`Invalid ${label}`);
}
}
function nextProviderPageToken(value: unknown): string | undefined {
if (value === undefined || value === null || value === '') return undefined;
if (
typeof value !== 'string' ||
value.length > PROVIDER_PAGE_TOKEN_MAX_LENGTH ||
/[\u0000-\u001f\u007f]/.test(value)
) {
throw new ConnectionEffectInvalidResponseError('Invalid provider model page token');
}
return value;
}
function classifyDiscoveryError(error: unknown): ConnectionEffectError {
if (error instanceof ConnectionEffectFetchError) return { kind: error.kind };
if (error instanceof ConnectionEffectHttpError) {
return classifyConnectionEffectStatus(error.status);
}
if (error instanceof ConnectionEffectInvalidResponseError || error instanceof SyntaxError) {
return { kind: 'invalid_response' };
}
return { kind: 'unknown' };
}