blob: 5c0b19eeb937d02c5635553b6b548a043517a85b [file]
import {
PROVIDER_DEFAULTS,
effectiveBaseUrl,
type ModelInfo,
type ProviderResponsesContract,
type ProviderRuntimeAdapter,
type ProviderType,
} from '@maka/core/llm-connections';
import { lookupModelProviderOverride, openAiAdapterApiProtocol } from '@maka/core/model-metadata';
import { isRetiredProvider } from '@maka/core/provider-registry';
import { resolveApplyPatchProfile, type ApplyPatchProfile } from './apply-patch-profile.js';
export type ModelRuntimeWire =
| 'anthropic-messages'
| 'openai-chat'
| 'openai-responses'
| 'google-generate'
| 'cohere-v2';
export type ReasoningReplayContract =
| { kind: 'none' }
| { kind: 'anthropic-signed' }
| { kind: 'openai-chat-plaintext'; requestField: 'observed' | 'reasoning' }
| { kind: 'responses'; contract: ProviderResponsesContract };
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;
/** Effective ApplyPatch contract after provider, model, and request wire are resolved. */
applyPatchProfile: ApplyPatchProfile | null;
}
export interface ModelRuntimeConnection {
readonly providerType: ProviderType;
readonly baseUrl?: string;
readonly models?: readonly ModelInfo[];
}
export function resolveModelRuntime(
connection: ModelRuntimeConnection,
modelId: string,
): ResolvedModelRuntime {
// Ahead of the override lookup: an override builds its own active adapter,
// so consulting it first would let a per-model entry hand a retired provider
// a working adapter and skip the `unavailable` refusal below entirely. No
// such entry exists today, but that table is generated from an external
// source — one row should not be able to undo a retirement.
if (isRetiredProvider(connection.providerType)) {
throw new Error(
`"${connection.providerType}" is retired and can no longer resolve a model runtime.`,
);
}
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
// `isRealConnection` 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: ProviderRuntimeAdapter =
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),
applyPatchProfile: resolveApplyPatchProfile(
{
wire,
applyPatchProtocol: adapter.applyPatchProtocol,
},
modelId,
),
};
}
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':
return 'anthropic-messages';
case 'unavailable':
// Reached only if something selected a retired provider despite the
// pickers filtering it out; failing here beats sending on a wire we
// cannot name.
throw new Error('This provider has no Runtime adapter and cannot resolve a wire.');
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.responses !== undefined &&
(apiProtocol ?? openAiAdapterApiProtocol(modelId, providerType)) === 'openai-responses'
? 'openai-responses'
: 'openai-chat';
case 'google':
return 'google-generate';
case 'cohere':
return 'cohere-v2';
}
}
function reasoningReplayContract(
adapter: ProviderRuntimeAdapter,
wire: ModelRuntimeWire,
): ReasoningReplayContract {
switch (wire) {
case 'anthropic-messages':
return { kind: 'anthropic-signed' };
case 'openai-responses':
return { kind: 'responses', contract: responsesContract(adapter) };
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':
return { kind: 'none' };
}
}
function responsesContract(adapter: ProviderRuntimeAdapter): ProviderResponsesContract {
if (adapter.kind === 'openai-compatible' && adapter.responses) return adapter.responses;
return { adapter: 'openai', reasoningReplay: 'encrypted-content' };
}
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`);
}
}