blob: 91836ddde9b3e8dbf2556ee7b7d78154b650fa1c [file]
import {
PROVIDER_DEFAULTS,
effectiveBaseUrl,
type ModelInfo,
type ProviderRuntimeAdapter,
type ProviderType,
} from '@maka/core';
import { lookupModelProviderOverride, openAiAdapterApiProtocol } from '@maka/core';
export type ModelRuntimeWire =
| 'anthropic-messages'
| 'openai-chat'
| 'openai-responses'
| 'google-generate'
| 'cohere-v2'
| 'unavailable';
export type ReasoningReplayContract =
| { kind: 'none' }
| { kind: 'anthropic-signed' }
| { kind: 'openai-chat-plaintext'; requestField: 'observed' | 'reasoning' }
| { kind: 'openai-responses-item' };
export interface ResolvedModelRuntime {
adapter: ProviderRuntimeAdapter;
baseUrl: string;
/** Account-advertised request wire for adapters that route per model. */
apiProtocol?: ModelInfo['apiProtocol'];
/** Effective wire after account, adapter, and model defaults are resolved. */
wire: ModelRuntimeWire;
/** Durable reasoning replay semantics carried by that wire. */
reasoningReplay: ReasoningReplayContract;
}
export interface ModelRuntimeConnection {
readonly providerType: ProviderType;
readonly baseUrl?: string;
readonly models?: readonly ModelInfo[];
}
export function resolveModelRuntime(
connection: ModelRuntimeConnection,
modelId: string,
): ResolvedModelRuntime {
const override = lookupModelProviderOverride(connection.providerType, modelId);
const defaults = PROVIDER_DEFAULTS[connection.providerType];
// Unknown providerType with no per-model override → can't resolve an adapter.
// Throw a clear error rather than crashing on `.runtimeAdapter`. Mirrors
// `isFakeBackend` in @maka/core/connection-readiness.ts.
if (!override && !defaults) {
throw new Error(
`Unknown provider type "${connection.providerType}"; cannot resolve model runtime.`,
);
}
const apiProtocol = connection.models?.find((model) => model.id === modelId)?.apiProtocol;
if (
connection.providerType === 'kimi-coding-plan' &&
apiProtocol !== undefined &&
apiProtocol !== 'anthropic-messages' &&
apiProtocol !== 'openai-chat'
) {
throw new Error(
`Kimi Coding Plan protocol must be openai-chat or anthropic-messages, received ${apiProtocol}`,
);
}
const adapter =
connection.providerType === 'kimi-coding-plan' && apiProtocol === 'openai-chat'
? ({
kind: 'openai-compatible',
name: 'provider',
includeUsage: true,
} as const)
: override
? runtimeAdapterOverride(override.npm)
: defaults.runtimeAdapter;
const configuredBaseUrl = connection.baseUrl?.trim();
const resolvedBaseUrl = configuredBaseUrl
? effectiveBaseUrl(connection)
: (override?.api ?? effectiveBaseUrl(connection));
const wire = resolveModelRuntimeWire(connection.providerType, modelId, adapter, apiProtocol);
return {
adapter,
baseUrl:
connection.providerType === 'kimi-coding-plan' && apiProtocol === 'openai-chat'
? kimiOpenAiBaseUrl(resolvedBaseUrl)
: resolvedBaseUrl,
...(apiProtocol ? { apiProtocol } : {}),
wire,
reasoningReplay: reasoningReplayContract(adapter, wire),
};
}
export function modelUsesAnthropicMessages(
connection: ModelRuntimeConnection,
modelId: string,
): boolean {
return resolveModelRuntime(connection, modelId).wire === 'anthropic-messages';
}
/** Native OpenAI lanes keep mutable continuation state inside ModelAdapter. */
export function modelUsesNativeOpenAiResponses(
connection: ModelRuntimeConnection,
modelId: string,
): boolean {
return (
connection.providerType === 'openai' &&
resolveModelRuntime(connection, modelId).wire === 'openai-responses'
);
}
function resolveModelRuntimeWire(
providerType: ProviderType,
modelId: string,
adapter: ProviderRuntimeAdapter,
apiProtocol: ModelInfo['apiProtocol'] | undefined,
): ModelRuntimeWire {
switch (adapter.kind) {
case 'anthropic':
case 'claude-subscription':
return 'anthropic-messages';
case 'openai-codex':
return 'openai-responses';
case 'github-copilot':
return apiProtocol === 'anthropic-messages'
? 'anthropic-messages'
: apiProtocol === 'openai-responses'
? 'openai-responses'
: 'openai-chat';
case 'openai':
return (adapter.apiProtocol ??
apiProtocol ??
openAiAdapterApiProtocol(modelId, providerType)) === 'openai-responses'
? 'openai-responses'
: 'openai-chat';
case 'openai-compatible':
return adapter.supportsOpenAiResponses === true &&
(apiProtocol ?? openAiAdapterApiProtocol(modelId, providerType)) === 'openai-responses'
? 'openai-responses'
: 'openai-chat';
case 'google':
return 'google-generate';
case 'cohere':
return 'cohere-v2';
case 'unavailable':
return 'unavailable';
}
}
function reasoningReplayContract(
adapter: ProviderRuntimeAdapter,
wire: ModelRuntimeWire,
): ReasoningReplayContract {
switch (wire) {
case 'anthropic-messages':
return { kind: 'anthropic-signed' };
case 'openai-responses':
return { kind: 'openai-responses-item' };
case 'openai-chat':
return adapter.kind === 'openai-compatible'
? {
kind: 'openai-chat-plaintext',
requestField:
adapter.replayAssistantReasoningAs === 'reasoning' ? 'reasoning' : 'observed',
}
: { kind: 'none' };
case 'google-generate':
case 'cohere-v2':
case 'unavailable':
return { kind: 'none' };
}
}
function kimiOpenAiBaseUrl(baseUrl: string): string {
return `${baseUrl.replace(/\/+$/, '').replace(/\/v1$/i, '')}/v1`;
}
function runtimeAdapterOverride(packageName: string): ProviderRuntimeAdapter {
switch (packageName) {
case '@ai-sdk/anthropic':
return { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true };
case '@ai-sdk/google':
return { kind: 'google', normalizeBaseUrl: false };
case '@ai-sdk/openai':
return { kind: 'openai' };
case '@ai-sdk/openai-compatible':
return { kind: 'openai-compatible', name: 'provider' };
default:
throw new Error(`models.dev model runtime package ${packageName} is unsupported`);
}
}