| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import type { ErrorEvent, CompleteEvent } from '@maka/core/events'; |
| import { openai } from '@ai-sdk/openai'; |
| import { anthropic } from '@ai-sdk/anthropic'; |
| import { |
| providerAuthRequiresSecret, |
| type RuntimeExecutionConnection, |
| } from '@maka/core/llm-connections'; |
| import { lookupModelMetadata } from '@maka/core/model-metadata'; |
| import type { CacheMissInputSource } from '@maka/core/usage-stats/types'; |
| import { rawFinishReasonString } from './model-protocol.js'; |
| import type { |
| ModelMessage, |
| NormalizedUsage, |
| RawUsageFields, |
| ModelStreamEvent, |
| ModelStreamResult, |
| ModelStepOutcome, |
| ModelFinishReason, |
| ModelFailure, |
| ModelFailureKind, |
| ModelToolSet, |
| ToolCallPart, |
| } from './model-protocol.js'; |
| export type { |
| NormalizedUsage, |
| RawUsageFields, |
| ModelStreamEvent, |
| ModelStreamResult, |
| ModelStepOutcome, |
| ModelFinishReason, |
| ModelFailure, |
| ModelToolSet, |
| } from './model-protocol.js'; |
| |
| import { resolveModelRuntime, type ResolvedModelRuntime } from './model-runtime.js'; |
| import { |
| plaintextResponsesReasoningProviderOptions, |
| safePlaintextResponsesReasoningItemId, |
| } from './responses-reasoning-state.js'; |
| import { classifyError, providerModelFailure } from './provider-error-classification.js'; |
| import { |
| withProviderStreamTracking, |
| type ProviderRequestTracker, |
| type ProviderStreamResult, |
| } from './provider-request-telemetry.js'; |
| import type { ContextDiagnosticsCompaction } from './context-diagnostics.js'; |
| import { |
| createOpenAiChatReasoningTransportState, |
| openAiChatReasoningFieldProviderOptions, |
| restoreOpenAiChatEmptyReasoning, |
| type OpenAiChatReasoningTransportState, |
| } from './openai-chat-reasoning-transport.js'; |
| import type { ModelFactoryInput } from './model-factory.js'; |
| import { |
| mergeOpenAiResponsesProviderOptions, |
| planOpenAiResponsesContinuation, |
| } from './openai-responses-continuation.js'; |
| import { |
| createOpenAiResponsesTransportState, |
| OPENAI_RESPONSES_LANE_HEADER, |
| type OpenAiResponsesTransportState, |
| } from './openai-responses-websocket.js'; |
| import { openAiApplyPatchProviderTool } from './openai-apply-patch.js'; |
| import { TOOL_SEARCH_NAME, TOOL_SEARCH_PROVIDER_NAME } from './tool-availability.js'; |
| |
| /** |
| * Build an ai-sdk LanguageModel from a single input object. |
| * Matches the signature exported by `runtime/model-factory.ts` (@kabi): |
| * `getAIModel(input: ModelFactoryInput): LanguageModelV2` |
| * |
| * We type-erase the return as `unknown` here to avoid pulling ai-sdk's |
| * `LanguageModelV2` type into core's dependency graph. |
| */ |
| export type { ModelFactoryInput }; |
| export type ModelFactory = (input: ModelFactoryInput) => unknown; |
| |
| export interface RepairableAiSdkToolCall { |
| toolCallId: string; |
| toolName: string; |
| input: string; |
| providerExecuted?: boolean; |
| providerMetadata?: unknown; |
| } |
| |
| export interface ModelAdapterInput { |
| sessionId?: string; |
| connection: RuntimeExecutionConnection; |
| apiKey: string; |
| modelId: string; |
| modelFactory: ModelFactory; |
| resolvedRuntime?: ResolvedModelRuntime; |
| providerOptions?: Record<string, unknown>; |
| newId: () => string; |
| now: () => number; |
| /** Test seam; production adapters own one state instance for their lifetime. */ |
| openAiResponsesTransportState?: OpenAiResponsesTransportState; |
| } |
| |
| export interface ModelAdapterStreamInput { |
| model: unknown; |
| messages: ModelMessage[]; |
| tools: ModelToolSet; |
| activeTools: string[]; |
| /** Observe each successfully pulled SDK stream part before semantic translation. */ |
| onStreamActivity: () => void; |
| system?: string; |
| abortSignal: AbortSignal; |
| repairToolCall: (input: { |
| toolCall: RepairableAiSdkToolCall; |
| error: unknown; |
| }) => RepairableAiSdkToolCall | null | Promise<RepairableAiSdkToolCall | null>; |
| /** Main-agent provider-call tracker. Auxiliary calls track their own generates. */ |
| providerRequestTracker?: ProviderRequestTracker; |
| /** |
| * The compaction boundary the messages of THIS call were projected under |
| * (#2323). Travels with the messages rather than being read from session |
| * state at settlement, which is why it is an argument here at all: the |
| * caller dispatches once per physical request and knows which fold each one |
| * was built from; nothing downstream can recover that afterwards. |
| */ |
| historyCompactBoundary?: ContextDiagnosticsCompaction; |
| /** Turn-scoped continuation lane. Omitted callers keep the full-request path. */ |
| continuationKey?: string; |
| } |
| |
| export class ModelAdapter { |
| private readonly runtime: ResolvedModelRuntime; |
| private readonly openAiChatReasoningTransportState: OpenAiChatReasoningTransportState; |
| private readonly openAiResponsesTransportState: OpenAiResponsesTransportState; |
| |
| constructor(private readonly input: ModelAdapterInput) { |
| this.runtime = input.resolvedRuntime ?? resolveModelRuntime(input.connection, input.modelId); |
| this.openAiChatReasoningTransportState = createOpenAiChatReasoningTransportState( |
| this.runtime.reasoningReplay.kind === 'openai-chat-plaintext' |
| ? this.runtime.reasoningReplay.requestField |
| : 'observed', |
| ); |
| this.openAiResponsesTransportState = |
| input.openAiResponsesTransportState ?? createOpenAiResponsesTransportState(); |
| } |
| |
| runtimeEventReplaySupport(): ModelAdapterRuntimeEventReplaySupport { |
| return { |
| toolCalls: true, |
| toolResults: true, |
| // Verified against @ai-sdk/open-responses@2.0.34: replay preserves |
| // item order and IDs, but a provider-executed result embedded in the |
| // assistant message (Maka's provider-tool chronology) is still dropped, |
| // leaving a dangling function_call on the wire. Fail closed until the |
| // upstream extension seam (vercel/ai#18899) can round-trip the pair. |
| providerExecutedTools: |
| this.runtime.reasoningReplay.kind !== 'responses' || |
| this.runtime.reasoningReplay.contract.adapter !== 'open-responses', |
| signedThinking: this.runtime.reasoningReplay.kind === 'anthropic-signed', |
| // openai-compatible transports replay stored reasoning unconditionally: |
| // DeepSeek-style endpoints 400 tool calls whose history lacks it, and |
| // relays that don't need the field ignore it. Reasoning is still |
| // recorded to the event log and rendered regardless. |
| unsignedThinking: this.runtime.reasoningReplay.kind === 'openai-chat-plaintext', |
| responsesReasoning: |
| this.runtime.reasoningReplay.kind !== 'responses' |
| ? 'none' |
| : this.runtime.reasoningReplay.contract.reasoningReplay === 'encrypted-content' |
| ? 'encrypted-content' |
| : this.runtime.reasoningReplay.contract.reasoningReplay === 'plaintext-content' |
| ? 'plaintext-content' |
| : { |
| kind: 'plaintext-item', |
| profile: requireResponsesReplayProfile(this.runtime), |
| providerOptionsKey: requireResponsesProviderOptionsKey(this.runtime), |
| }, |
| }; |
| } |
| |
| resolveModel(): unknown { |
| if (providerAuthRequiresSecret(this.input.connection.providerType) && !this.input.apiKey) { |
| throw new Error(`No API key stored for connection "${this.input.connection.slug}"`); |
| } |
| return this.input.modelFactory({ |
| sessionId: this.input.sessionId, |
| connection: this.input.connection, |
| apiKey: this.input.apiKey, |
| modelId: this.input.modelId, |
| resolvedRuntime: this.runtime, |
| ...(this.runtime.reasoningReplay.kind === 'openai-chat-plaintext' |
| ? { openAiChatReasoningTransportState: this.openAiChatReasoningTransportState } |
| : {}), |
| ...(usesNativeOpenAiResponses(this.input.connection, this.runtime) |
| ? { openAiResponsesTransportState: this.openAiResponsesTransportState } |
| : {}), |
| }); |
| } |
| |
| maxOutputTokens(): number | undefined { |
| return selectedModelMaxOutputTokens( |
| this.input.connection, |
| this.input.modelId, |
| this.input.providerOptions, |
| this.runtime, |
| ); |
| } |
| |
| async startStream(input: ModelAdapterStreamInput): Promise<ModelStreamResult> { |
| const ai = await import('ai').catch((err) => { |
| throw new Error( |
| `Failed to load 'ai' package. Run \`npm install ai\`. Inner: ${(err as Error).message}`, |
| ); |
| }); |
| const { streamText, wrapLanguageModel } = ai as unknown as { |
| streamText: (opts: Record<string, unknown>) => SdkStreamResult; |
| wrapLanguageModel: (input: Record<string, unknown>) => unknown; |
| }; |
| |
| const maxOutputTokens = selectedModelMaxOutputTokens( |
| this.input.connection, |
| this.input.modelId, |
| this.input.providerOptions, |
| this.runtime, |
| ); |
| let settleAccounting: ((outcome: ModelStepOutcome) => Promise<void>) | undefined; |
| const terminalModel = withProviderFinishBoundary(input.model, wrapLanguageModel); |
| const trackedModel = input.providerRequestTracker |
| ? withProviderStreamTracking({ |
| model: terminalModel, |
| wrapLanguageModel, |
| tracker: input.providerRequestTracker, |
| abortSignal: input.abortSignal, |
| onAttempt: (settle) => { |
| settleAccounting = settle; |
| }, |
| ...(input.historyCompactBoundary |
| ? { historyCompactBoundary: input.historyCompactBoundary } |
| : {}), |
| }) |
| : terminalModel; |
| const usesOpenAiResponsesAdapter = hasOpenAiResponsesAdapter(this.runtime); |
| const providerToolName = (name: string): string => |
| usesOpenAiResponsesAdapter && name === TOOL_SEARCH_NAME ? TOOL_SEARCH_PROVIDER_NAME : name; |
| const runtimeToolName = (name: string): string => |
| usesOpenAiResponsesAdapter && name === TOOL_SEARCH_PROVIDER_NAME ? TOOL_SEARCH_NAME : name; |
| const sdkTools = lowerModelTools(input.tools); |
| if (usesOpenAiResponsesAdapter && sdkTools[TOOL_SEARCH_NAME] !== undefined) { |
| sdkTools[TOOL_SEARCH_PROVIDER_NAME] = sdkTools[TOOL_SEARCH_NAME]; |
| delete sdkTools[TOOL_SEARCH_NAME]; |
| } |
| const fullMessages = input.messages; |
| const responsesLane = |
| input.continuationKey && usesNativeOpenAiResponses(this.input.connection, this.runtime) |
| ? input.continuationKey |
| : undefined; |
| const continuation = responsesLane |
| ? planOpenAiResponsesContinuation( |
| fullMessages, |
| this.openAiResponsesTransportState.semanticBaseline(responsesLane), |
| ) |
| : { messages: fullMessages }; |
| const providerMessages = remapModelMessageToolNames(continuation.messages, providerToolName); |
| const providerSystem = input.system |
| ? remapProviderToolNamesInText(input.system, providerToolName) |
| : undefined; |
| const providerOptions = usesNativeOpenAiResponses(this.input.connection, this.runtime) |
| ? mergeOpenAiResponsesProviderOptions( |
| this.input.providerOptions, |
| this.input.sessionId ?? this.input.connection.slug, |
| continuation.previousResponseId, |
| ) |
| : this.input.providerOptions; |
| const sdkResult = streamText({ |
| model: trackedModel, |
| messages: providerMessages, |
| tools: sdkTools, |
| activeTools: input.activeTools.map(providerToolName), |
| // An empty active set is an authoritative tool-free request (not merely |
| // an empty provider schema). Some OpenAI-compatible models, including |
| // DeepSeek, otherwise keep emitting their native tool-call envelope as |
| // ordinary text when the SDK leaves toolChoice at its `auto` default. |
| // The child-agent finalization step relies on this boundary to spend its |
| // last budgeted request on a summary instead of one more unusable call. |
| ...(input.activeTools.length === 0 ? { toolChoice: 'none' } : {}), |
| repairToolCall: async ({ |
| toolCall, |
| error, |
| }: { |
| toolCall: RepairableAiSdkToolCall; |
| error: unknown; |
| }) => { |
| const repaired = await input.repairToolCall({ |
| toolCall: { ...toolCall, toolName: runtimeToolName(toolCall.toolName) }, |
| error, |
| }); |
| return repaired ? { ...repaired, toolName: providerToolName(repaired.toolName) } : repaired; |
| }, |
| ...(providerSystem ? { instructions: providerSystem } : {}), |
| ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), |
| providerOptions, |
| ...(responsesLane ? { headers: { [OPENAI_RESPONSES_LANE_HEADER]: responsesLane } } : {}), |
| maxRetries: 0, |
| // With no continuation predicate, streamText performs one provider step. |
| // Continuation belongs to the Runtime above this adapter. |
| abortSignal: input.abortSignal, |
| // The SDK default onError console.errors the raw error object (stack, |
| // request bodies), which lands on the terminal outside the TUI |
| // transcript. Stream failures already surface through the stream |
| // `error` event → ErrorEvent path, so silence the default. |
| onError: () => {}, |
| }) as unknown as SdkStreamResult; |
| return this.toModelStreamResult(sdkResult, input.onStreamActivity, { |
| ...(responsesLane ? { lane: responsesLane } : {}), |
| requestMessages: fullMessages, |
| abortSignal: input.abortSignal, |
| runtimeToolName, |
| settleAccounting: async (outcome) => { |
| await settleAccounting?.(outcome); |
| }, |
| }); |
| } |
| |
| /** |
| * Lower an AI SDK `streamText` result into the Maka-owned `ModelStreamResult`. |
| * The raw SDK chunk stream is translated lazily to `ModelStreamEvent`s so |
| * streaming stays live; failures, usage, finish reason, and request messages |
| * are normalized to Maka-owned contracts. No AI SDK type escapes this method. |
| */ |
| private toModelStreamResult( |
| sdk: SdkStreamResult, |
| onStreamActivity: () => void, |
| continuation: { |
| lane?: string; |
| requestMessages: ModelMessage[]; |
| abortSignal: AbortSignal; |
| runtimeToolName?: (name: string) => string; |
| settleAccounting: (outcome: ModelStepOutcome) => Promise<void>; |
| }, |
| ): ModelStreamResult { |
| const openAiChatReasoningTransportState = |
| this.runtime.reasoningReplay.kind === 'openai-chat-plaintext' |
| ? this.openAiChatReasoningTransportState |
| : undefined; |
| const openAiResponsesTransportState = this.openAiResponsesTransportState; |
| const resolvedRuntime = this.runtime; |
| let settleOutcome!: (outcome: ModelStepOutcome) => void; |
| const outcome = new Promise<ModelStepOutcome>((resolve) => { |
| settleOutcome = resolve; |
| }); |
| const events: AsyncIterable<ModelStreamEvent> = { |
| async *[Symbol.asyncIterator]() { |
| let failure: ModelFailure | undefined; |
| let sawFinish = false; |
| let streamedFinishReason: string | undefined; |
| let streamedRawFinishReason: string | undefined; |
| let sawUnfinalizedPlaintextSummary = false; |
| try { |
| for await (const chunk of sdk.stream as AsyncIterable<AiSdkStreamChunk>) { |
| onStreamActivity(); |
| if ( |
| chunk.type === 'finish' || |
| chunk.type === 'finish-step' || |
| chunk.type === 'step-finish' |
| ) { |
| streamedRawFinishReason = |
| rawFinishReasonString(chunk.rawFinishReason) ?? streamedRawFinishReason; |
| streamedFinishReason = chunkFinishReason(chunk) ?? streamedFinishReason; |
| if (chunk.type === 'finish') sawFinish = true; |
| continue; |
| } |
| if (isUnfinalizedPlaintextSummaryReasoningEnd(chunk, resolvedRuntime)) { |
| // The SDK emits this trailer from flush() when no |
| // response.output_item.done finalized the active item. Defer the |
| // decision until the terminal outcome is known: an existing |
| // provider failure must win, while a successful stream must |
| // still fail closed instead of losing replay state silently. |
| sawUnfinalizedPlaintextSummary = true; |
| continue; |
| } |
| for (const event of translateChunk( |
| chunk, |
| openAiChatReasoningTransportState, |
| resolvedRuntime, |
| continuation.runtimeToolName, |
| )) { |
| if (event.kind === 'error') failure = event.failure; |
| yield event; |
| } |
| } |
| } catch (error) { |
| if (!failure) { |
| failure = normalizeProviderFailure(error); |
| yield { kind: 'error', failure }; |
| } |
| } finally { |
| if (continuation.abortSignal.aborted) { |
| failure = normalizeProviderFailure(continuation.abortSignal.reason); |
| } |
| const [sdkUsage, sdkFinishReason] = await Promise.all([ |
| sdk.usage.catch(() => undefined), |
| sdk.finishReason.catch(() => undefined), |
| ]); |
| // An early-stopping consumer (a provider-mismatch throw, a user |
| // stop) ends this stream before any finish chunk exists, so the SDK |
| // rejects every result promise during teardown. `usage` and |
| // `finishReason` are consumed above; `response` is only read on the |
| // completed continuation path below. Sink it unconditionally so the |
| // error path can never surface an unhandled rejection after the |
| // turn unwinds — the timing of that settlement is scheduler-owned |
| // (observed post-test on Windows), and Node's default makes an |
| // unhandled rejection a crash. |
| void Promise.resolve(sdk.response).catch(() => undefined); |
| const finishReason = |
| streamedFinishReason ?? rawFinishReasonString(sdkFinishReason) ?? 'unknown'; |
| const rawFinishReason = |
| streamedRawFinishReason ?? rawFinishReasonString(sdkFinishReason) ?? finishReason; |
| const usage = normalizeAiSdkUsage(sdkUsage, { rawFinishReason }); |
| let settled = settleModelStepOutcome({ |
| aborted: continuation.abortSignal.aborted, |
| failure, |
| sawFinish, |
| finishReason, |
| rawFinishReason, |
| usage, |
| }); |
| let deferredFailure: ModelFailure | undefined; |
| |
| if (sawUnfinalizedPlaintextSummary && settled.kind === 'completed') { |
| failure = normalizeProviderFailure( |
| new Error('Plaintext Responses reasoning item is missing final summary metadata'), |
| ); |
| deferredFailure = failure; |
| settled = settleModelStepOutcome({ |
| aborted: continuation.abortSignal.aborted, |
| failure, |
| sawFinish, |
| finishReason, |
| rawFinishReason, |
| usage, |
| }); |
| } |
| |
| try { |
| if (continuation.lane) { |
| if (settled.kind === 'completed') { |
| const response = await Promise.resolve(sdk.response).catch(() => undefined); |
| if ( |
| response?.id && |
| openAiResponsesTransportState.canRecordSemantic(continuation.lane, response.id) |
| ) { |
| openAiResponsesTransportState.recordSemanticRequest(continuation.lane, { |
| requestMessages: structuredClone(continuation.requestMessages), |
| responseId: response.id, |
| }); |
| settled = { ...settled, continuation: 'pending' }; |
| } else { |
| openAiResponsesTransportState.clearSemantic(continuation.lane); |
| } |
| } else { |
| openAiResponsesTransportState.clearSemantic(continuation.lane); |
| } |
| } |
| } finally { |
| try { |
| await continuation.settleAccounting(settled); |
| } finally { |
| settleOutcome(settled); |
| } |
| } |
| if (deferredFailure) { |
| // Consumers may stop iterating at the first error. The outcome and |
| // continuation lane must already be settled before this yield so a |
| // generator return cannot strand the caller awaiting result.outcome. |
| yield { kind: 'error', failure: deferredFailure }; |
| } |
| } |
| }, |
| }; |
| return { events, outcome }; |
| } |
| |
| endContinuation(lane: string): void { |
| this.openAiResponsesTransportState.endLane(lane); |
| } |
| |
| recordContinuationResponse(lane: string, responseMessages: readonly ModelMessage[]): void { |
| this.openAiResponsesTransportState.recordSemanticResponse(lane, responseMessages); |
| } |
| |
| clearContinuation(lane: string): void { |
| this.openAiResponsesTransportState.clearSemantic(lane); |
| } |
| |
| dispose(): void { |
| this.openAiResponsesTransportState.close(); |
| } |
| |
| /** |
| * Translate one raw AI SDK stream chunk into zero or more Maka-owned |
| * `ModelStreamEvent`s. This is the sole place that parses SDK chunk names |
| * (`text-delta` / `reasoning-delta` / `finish-step` / `finish` / `error` / …); |
| * the backend never sees them. Pure and side-effect-free so it is directly |
| * testable through the Maka-owned event contract. |
| */ |
| translateChunk(chunk: AiSdkStreamChunk): ModelStreamEvent[] { |
| return translateChunk( |
| chunk, |
| this.runtime.reasoningReplay.kind === 'openai-chat-plaintext' |
| ? this.openAiChatReasoningTransportState |
| : undefined, |
| this.runtime, |
| ); |
| } |
| |
| makeErrorEvent(turnId: string, err: unknown, reasonOverride?: string): ErrorEvent { |
| const failure = normalizeProviderFailure(err); |
| return { |
| type: 'error', |
| id: this.input.newId(), |
| turnId, |
| ts: this.input.now(), |
| recoverable: false, |
| ...(failure.code !== undefined ? { code: failure.code } : {}), |
| ...(reasonOverride !== undefined |
| ? { reason: reasonOverride } |
| : failure.kind !== 'abort' |
| ? { reason: failure.kind } |
| : {}), |
| message: failure.message, |
| }; |
| } |
| |
| normalizeFailure(error: unknown): ModelFailure { |
| return normalizeProviderFailure(error); |
| } |
| |
| classifyError(error: unknown): ModelFailureKind { |
| if (isModelFailure(error)) return error.kind; |
| return classifyError(error); |
| } |
| |
| /** Map a successfully settled provider step to its runtime stop reason. */ |
| mapFinishReason(reason: ModelFinishReason): CompleteEvent['stopReason'] { |
| switch (reason) { |
| case 'stop': |
| return 'end_turn'; |
| case 'length': |
| return 'max_tokens'; |
| case 'tool-calls': |
| return 'end_turn'; |
| default: |
| return 'end_turn'; |
| } |
| } |
| } |
| |
| interface ModelStepSettlementEvidence { |
| aborted: boolean; |
| failure?: ModelFailure; |
| sawFinish: boolean; |
| finishReason: ModelFinishReason; |
| rawFinishReason?: string; |
| usage?: NormalizedUsage; |
| } |
| |
| export function settleModelStepOutcome(evidence: ModelStepSettlementEvidence): ModelStepOutcome { |
| const { aborted, failure, sawFinish, finishReason, rawFinishReason, usage } = evidence; |
| if (aborted || failure?.kind === 'abort') { |
| return failedStepOutcome( |
| failure ?? |
| normalizeProviderFailure(Object.assign(new Error('aborted'), { name: 'AbortError' })), |
| usage, |
| ); |
| } |
| if (failure) { |
| return failedStepOutcome(failure, usage); |
| } |
| if (!sawFinish || finishReason === 'other' || finishReason === 'unknown') { |
| return failedStepOutcome( |
| modelStepFailure( |
| 'stream_truncated', |
| `Provider stream ended without finishing (${finishReason})`, |
| ), |
| usage, |
| ); |
| } |
| if (finishReason === 'content-filter' || finishReason === 'error') { |
| const terminalFailure = |
| finishReason === 'error' |
| ? providerFinishFailure(rawFinishReason) |
| : modelStepFailure('unknown', 'Provider stopped the stream on a content filter'); |
| return failedStepOutcome(terminalFailure, usage); |
| } |
| return { |
| kind: 'completed', |
| finishReason, |
| ...(usage ? { usage } : {}), |
| continuation: 'none', |
| }; |
| } |
| |
| function modelStepFailure(kind: ModelFailureKind, message: string): ModelFailure { |
| return { type: 'model_failure', kind, message, retryable: kind === 'stream_truncated' }; |
| } |
| |
| function providerFinishFailure(rawFinishReason: string | undefined): ModelFailure { |
| if (rawFinishReason && rawFinishReason !== 'error') { |
| const normalized = normalizeProviderFailure({ |
| code: rawFinishReason, |
| message: 'Provider stopped the stream with an error', |
| }); |
| // A finish reason carries no request-level Retry-After or transport |
| // evidence. Preserve its classification for diagnostics without widening |
| // the pre-existing retry policy for every provider. |
| if (normalized.kind !== 'unknown') return { ...normalized, retryable: false }; |
| return { |
| ...modelStepFailure('provider_unavailable', 'Provider stopped the stream with an error'), |
| ...(normalized.code ? { code: normalized.code } : {}), |
| }; |
| } |
| return modelStepFailure('provider_unavailable', 'Provider stopped the stream with an error'); |
| } |
| |
| function failedStepOutcome( |
| failure: ModelFailure, |
| usage?: NormalizedUsage, |
| ): Exclude<ModelStepOutcome, { kind: 'completed' }> { |
| return { |
| kind: 'failed', |
| failure, |
| ...(usage ? { usage } : {}), |
| continuation: 'none', |
| }; |
| } |
| |
| function selectedModelMaxOutputTokens( |
| connection: RuntimeExecutionConnection, |
| modelId: string, |
| providerOptions: Record<string, unknown> | undefined, |
| runtime: ResolvedModelRuntime, |
| ): number | undefined { |
| const anthropicMessages = runtime.wire === 'anthropic-messages'; |
| const kimiOpenAiChat = |
| connection.providerType === 'kimi-coding-plan' && runtime.wire === 'openai-chat'; |
| const requestedBudget = connection.modelOverrides?.[modelId]?.maxOutputTokens; |
| if (requestedBudget === undefined && !anthropicMessages && !kimiOpenAiChat) return undefined; |
| const capacity = |
| connection.models?.find((model) => model.id === modelId)?.maxOutputTokens ?? |
| lookupModelMetadata(connection.providerType, modelId).maxOutputTokens; |
| const wireOutputLimit = |
| requestedBudget === undefined |
| ? capacity |
| : capacity === undefined |
| ? requestedBudget |
| : Math.min(requestedBudget, capacity); |
| if (wireOutputLimit === undefined) return undefined; |
| const outputTokens = anthropicMessages |
| ? wireOutputLimit - fixedAnthropicThinkingBudget(providerOptions) |
| : wireOutputLimit; |
| if (outputTokens <= 0) { |
| throw new Error( |
| 'Output budget must exceed the fixed thinking budget. Increase the output limit or reduce thinking.', |
| ); |
| } |
| return outputTokens; |
| } |
| |
| function usesNativeOpenAiResponses( |
| connection: RuntimeExecutionConnection, |
| runtime: ResolvedModelRuntime, |
| ): boolean { |
| return connection.providerType === 'openai' && runtime.wire === 'openai-responses'; |
| } |
| |
| function hasOpenAiResponsesAdapter(runtime: ResolvedModelRuntime): boolean { |
| return ( |
| runtime.wire === 'openai-responses' && |
| runtime.reasoningReplay.kind === 'responses' && |
| runtime.reasoningReplay.contract.adapter === 'openai' |
| ); |
| } |
| |
| function fixedAnthropicThinkingBudget( |
| providerOptions: Record<string, unknown> | undefined, |
| ): number { |
| const anthropic = providerOptions?.anthropic; |
| if (!anthropic || typeof anthropic !== 'object' || Array.isArray(anthropic)) return 0; |
| const thinking = (anthropic as { thinking?: unknown }).thinking; |
| if (!thinking || typeof thinking !== 'object' || Array.isArray(thinking)) return 0; |
| const { type, budgetTokens } = thinking as { type?: unknown; budgetTokens?: unknown }; |
| return type === 'enabled' && typeof budgetTokens === 'number' ? budgetTokens : 0; |
| } |
| |
| export interface ModelAdapterRuntimeEventReplaySupport { |
| toolCalls: boolean; |
| toolResults: boolean; |
| providerExecutedTools: boolean; |
| signedThinking: boolean; |
| unsignedThinking: boolean; |
| responsesReasoning: |
| | 'none' |
| | 'encrypted-content' |
| | 'plaintext-content' |
| | { |
| kind: 'plaintext-item'; |
| profile: string; |
| providerOptionsKey: string; |
| }; |
| } |
| |
| function requireResponsesProviderOptionsKey(runtime: ResolvedModelRuntime): string { |
| if (!runtime.responsesProviderOptionsKey) { |
| throw new Error('Plaintext Responses replay requires a provider-options key'); |
| } |
| return runtime.responsesProviderOptionsKey; |
| } |
| |
| function requireResponsesReplayProfile(runtime: ResolvedModelRuntime): string { |
| if (!runtime.responsesReplayProfile) { |
| throw new Error('Plaintext Responses replay requires a source profile'); |
| } |
| return runtime.responsesReplayProfile; |
| } |
| |
| /** |
| * Internal, adapter-only shape of an AI SDK `streamText` stream chunk. This |
| * type never crosses the `ModelAdapter` boundary — `ModelAdapter.translateChunk` |
| * consumes it and emits the Maka-owned `ModelStreamEvent`. It mirrors the AI |
| * SDK chunk union just enough to read the fields Maka cares about. |
| */ |
| interface AiSdkStreamChunk { |
| type: string; |
| id?: unknown; |
| text?: string; |
| delta?: string; |
| textDelta?: string; |
| toolCallId?: string; |
| toolName?: string; |
| input?: unknown; |
| args?: unknown; |
| providerExecuted?: boolean; |
| result?: unknown; |
| output?: unknown; |
| isError?: boolean; |
| usage?: AiSdkUsageLike; |
| finishReason?: unknown; |
| /** What the provider itself called it, before the SDK bucketed it. */ |
| rawFinishReason?: unknown; |
| error?: unknown; |
| /** Provider-specific metadata; carries the Anthropic reasoning signature. */ |
| providerMetadata?: unknown; |
| } |
| |
| /** |
| * Internal, adapter-only shape of an AI SDK `streamText` result. The public |
| * boundary contract is `ModelStreamResult`; this exists only to type the |
| * lowering cast inside `ModelAdapter`. |
| */ |
| interface SdkStreamResult { |
| stream: AsyncIterable<AiSdkStreamChunk>; |
| usage: Promise<AiSdkUsageLike | undefined>; |
| finishReason: Promise<unknown>; |
| response: PromiseLike<{ |
| id: string; |
| }>; |
| } |
| |
| /** |
| * A provider `finish` part is the LanguageModel stream's terminal semantic |
| * boundary. Expose EOF at that boundary so the SDK can flush its public |
| * finish/usage promises even when the transport keeps the connection open. |
| */ |
| function withProviderFinishBoundary( |
| model: unknown, |
| wrapLanguageModel: (input: Record<string, unknown>) => unknown, |
| ): unknown { |
| return wrapLanguageModel({ |
| model, |
| middleware: { |
| wrapStream: async ({ |
| doStream, |
| }: { |
| doStream: () => PromiseLike<ProviderStreamResult>; |
| }): Promise<ProviderStreamResult> => { |
| const result = await doStream(); |
| return { |
| ...result, |
| stream: result.stream.pipeThrough( |
| new TransformStream<unknown, unknown>({ |
| transform(part, controller) { |
| controller.enqueue(part); |
| if ( |
| part !== null && |
| typeof part === 'object' && |
| !Array.isArray(part) && |
| (part as { type?: unknown }).type === 'finish' |
| ) { |
| controller.terminate(); |
| } |
| }, |
| }), |
| ), |
| }; |
| }, |
| }, |
| }); |
| } |
| |
| /** |
| * The finish reason to forward, preferring what the provider actually said. |
| * |
| * The SDK splits the reason in two: a closed unified enum, and the provider's |
| * own spelling. Unified is the right thing to forward — `RuntimeKernel` and |
| * the backend compare against `'tool-calls'`, which is a name only the SDK |
| * uses. Except when unified is `other`, which is not a reason but the SDK |
| * declining to name one; there it hides the only distinction that matters |
| * downstream. `other` with a provider spelling is a model that stopped for a |
| * reason we have no case for — an ordinary finished turn. `other` with nothing |
| * behind it is a stream that died without anyone saying so. |
| */ |
| function chunkFinishReason(chunk: AiSdkStreamChunk): string | undefined { |
| const unified = rawFinishReasonString(chunk.finishReason); |
| if (unified !== 'other' && unified !== 'unknown') return unified; |
| return rawFinishReasonString(chunk.rawFinishReason) ?? unified; |
| } |
| |
| /** |
| * Extract the provider-signed reasoning signature from a stream chunk. |
| * Anthropic delivers it via `providerMetadata.anthropic.signature`; other |
| * providers omit it and this returns undefined. |
| */ |
| function reasoningSignatureFromChunk(chunk: AiSdkStreamChunk): string | undefined { |
| const meta = chunk.providerMetadata; |
| if (!meta || typeof meta !== 'object') return undefined; |
| const anthropic = (meta as { anthropic?: unknown }).anthropic; |
| if (!anthropic || typeof anthropic !== 'object') return undefined; |
| const signature = (anthropic as { signature?: unknown }).signature; |
| return typeof signature === 'string' && signature.length > 0 ? signature : undefined; |
| } |
| |
| function anthropicRedactedThinkingProviderOptionsFromChunk( |
| chunk: AiSdkStreamChunk, |
| ): NonNullable<ModelMessage['providerOptions']> | undefined { |
| const meta = chunk.providerMetadata; |
| if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return undefined; |
| const anthropic = (meta as { anthropic?: unknown }).anthropic; |
| if (!anthropic || typeof anthropic !== 'object' || Array.isArray(anthropic)) return undefined; |
| const redactedData = (anthropic as { redactedData?: unknown }).redactedData; |
| return typeof redactedData === 'string' |
| ? (meta as NonNullable<ModelMessage['providerOptions']>) |
| : undefined; |
| } |
| |
| function openAiResponsesReasoningProviderOptionsFromChunk( |
| chunk: AiSdkStreamChunk, |
| runtime: ResolvedModelRuntime, |
| ): NonNullable<ModelMessage['providerOptions']> | undefined { |
| const meta = chunk.providerMetadata; |
| if ( |
| runtime.reasoningReplay.kind === 'responses' && |
| runtime.reasoningReplay.contract.reasoningReplay === 'plaintext-summary' |
| ) { |
| if (chunk.type !== 'reasoning' && chunk.type !== 'reasoning-end') return undefined; |
| const providerOptionsKey = runtime.responsesProviderOptionsKey; |
| const provider = |
| providerOptionsKey && meta && typeof meta === 'object' |
| ? (meta as Record<string, unknown>)[providerOptionsKey] |
| : undefined; |
| const metadataItemIdValue = |
| provider && typeof provider === 'object' && !Array.isArray(provider) |
| ? (provider as { itemId?: unknown }).itemId |
| : undefined; |
| const streamItemIdValue = (chunk as { id?: unknown }).id; |
| if ( |
| typeof metadataItemIdValue === 'string' && |
| typeof streamItemIdValue === 'string' && |
| metadataItemIdValue !== streamItemIdValue |
| ) { |
| throw new Error('Plaintext Responses reasoning item id changed within one stream item'); |
| } |
| const itemId = |
| safePlaintextResponsesReasoningItemId(metadataItemIdValue) ?? |
| safePlaintextResponsesReasoningItemId(streamItemIdValue); |
| const summaryParts = plaintextSummaryParts(provider); |
| if (!itemId || !summaryParts) { |
| throw new Error('Plaintext Responses reasoning item is missing final summary metadata'); |
| } |
| const providerOptions = plaintextResponsesReasoningProviderOptions( |
| itemId, |
| requireResponsesReplayProfile(runtime), |
| summaryParts, |
| ); |
| if (!providerOptions) { |
| throw new Error('Plaintext Responses reasoning summary exceeds durable state bounds'); |
| } |
| return providerOptions; |
| } |
| if (!meta || typeof meta !== 'object') return undefined; |
| const openai = (meta as { openai?: unknown }).openai; |
| if (!openai || typeof openai !== 'object' || Array.isArray(openai)) return undefined; |
| const { itemId, reasoningEncryptedContent } = openai as { |
| itemId?: unknown; |
| reasoningEncryptedContent?: unknown; |
| }; |
| if (typeof itemId !== 'string' || itemId.length === 0) return undefined; |
| return { |
| openai: { |
| itemId, |
| ...(typeof reasoningEncryptedContent === 'string' || reasoningEncryptedContent === null |
| ? { reasoningEncryptedContent } |
| : {}), |
| }, |
| }; |
| } |
| |
| function isUnfinalizedPlaintextSummaryReasoningEnd( |
| chunk: AiSdkStreamChunk, |
| runtime: ResolvedModelRuntime, |
| ): boolean { |
| return ( |
| runtime.reasoningReplay.kind === 'responses' && |
| runtime.reasoningReplay.contract.reasoningReplay === 'plaintext-summary' && |
| chunk.type === 'reasoning-end' && |
| (chunk.providerMetadata === undefined || chunk.providerMetadata === null) |
| ); |
| } |
| |
| function plaintextSummaryParts(provider: unknown): string[] | undefined { |
| if (!provider || typeof provider !== 'object' || Array.isArray(provider)) return undefined; |
| const summary = (provider as { reasoningSummary?: unknown }).reasoningSummary; |
| if (!Array.isArray(summary)) return undefined; |
| const parts: string[] = []; |
| for (const part of summary) { |
| if (!part || typeof part !== 'object' || Array.isArray(part)) return undefined; |
| const { type, text } = part as { type?: unknown; text?: unknown }; |
| if (type !== 'summary_text' || typeof text !== 'string') return undefined; |
| parts.push(text); |
| } |
| return parts; |
| } |
| |
| function plaintextSummaryTextFromChunk( |
| chunk: AiSdkStreamChunk, |
| runtime: ResolvedModelRuntime | undefined, |
| ): string | undefined { |
| if ( |
| runtime?.reasoningReplay.kind !== 'responses' || |
| runtime.reasoningReplay.contract.reasoningReplay !== 'plaintext-summary' || |
| (chunk.type !== 'reasoning' && chunk.type !== 'reasoning-end') |
| ) { |
| return undefined; |
| } |
| const providerOptionsKey = runtime.responsesProviderOptionsKey; |
| const meta = chunk.providerMetadata; |
| const provider = |
| providerOptionsKey && meta && typeof meta === 'object' |
| ? (meta as Record<string, unknown>)[providerOptionsKey] |
| : undefined; |
| return plaintextSummaryParts(provider)?.join(''); |
| } |
| |
| function reasoningPartIdFromChunk(chunk: AiSdkStreamChunk): string | undefined { |
| return typeof chunk.id === 'string' && chunk.id.length > 0 && chunk.id.length <= 512 |
| ? chunk.id |
| : undefined; |
| } |
| |
| /** |
| * Translate one raw AI SDK stream chunk into zero or more Maka-owned |
| * `ModelStreamEvent`s. The sole site that parses SDK chunk names; the backend |
| * never sees raw chunks. Pure and side-effect-free. |
| */ |
| function translateChunk( |
| chunk: AiSdkStreamChunk, |
| openAiChatReasoningTransportState?: OpenAiChatReasoningTransportState, |
| runtime?: ResolvedModelRuntime, |
| runtimeToolName?: (name: string) => string, |
| ): ModelStreamEvent[] { |
| switch (chunk.type) { |
| case 'reasoning-start': { |
| const reasoningPartId = reasoningPartIdFromChunk(chunk); |
| const redactedThinkingProviderOptions = |
| anthropicRedactedThinkingProviderOptionsFromChunk(chunk); |
| return [ |
| { |
| kind: 'thinking-start', |
| ...(redactedThinkingProviderOptions |
| ? { providerOptions: redactedThinkingProviderOptions } |
| : {}), |
| ...(reasoningPartId ? { reasoningPartId } : {}), |
| }, |
| ]; |
| } |
| case 'text-start': { |
| const providerOptions = |
| chunk.providerMetadata && typeof chunk.providerMetadata === 'object' |
| ? (chunk.providerMetadata as NonNullable<ModelMessage['providerOptions']>) |
| : undefined; |
| const providerItemBoundary = |
| runtime !== undefined && |
| hasOpenAiResponsesAdapter(runtime) && |
| providerOptions !== undefined; |
| return [ |
| { |
| kind: 'text-start', |
| ...(providerItemBoundary ? { providerItemBoundary: true } : {}), |
| }, |
| ]; |
| } |
| case 'text-delta': { |
| const text = chunk.text ?? chunk.textDelta ?? chunk.delta ?? ''; |
| return text ? [{ kind: 'text', text }] : []; |
| } |
| case 'text-end': { |
| const providerOptions = |
| chunk.providerMetadata && typeof chunk.providerMetadata === 'object' |
| ? (chunk.providerMetadata as NonNullable<ModelMessage['providerOptions']>) |
| : undefined; |
| const providerItemBoundary = |
| runtime !== undefined && |
| hasOpenAiResponsesAdapter(runtime) && |
| providerOptions !== undefined; |
| return [ |
| { |
| kind: 'text-end', |
| ...(providerOptions !== undefined ? { providerOptions } : {}), |
| ...(providerItemBoundary ? { providerItemBoundary: true } : {}), |
| }, |
| ]; |
| } |
| case 'reasoning': |
| case 'reasoning-delta': { |
| const text = |
| typeof chunk.text === 'string' |
| ? chunk.text |
| : typeof chunk.textDelta === 'string' |
| ? chunk.textDelta |
| : typeof chunk.delta === 'string' |
| ? chunk.delta |
| : undefined; |
| const signature = reasoningSignatureFromChunk(chunk); |
| const responsesProviderOptions = runtime |
| ? openAiResponsesReasoningProviderOptionsFromChunk(chunk, runtime) |
| : undefined; |
| const reasoningPartId = reasoningPartIdFromChunk(chunk); |
| const reasoningSummaryText = plaintextSummaryTextFromChunk(chunk, runtime); |
| const events: ModelStreamEvent[] = []; |
| if (signature) { |
| events.push({ |
| kind: 'thinking-signature', |
| signature, |
| ...(reasoningPartId ? { reasoningPartId } : {}), |
| }); |
| } |
| // The signed reasoning chunk arrives as a standalone delta with empty |
| // text; preserve provider-authored empty reasoning, but do not surface a |
| // signature-only carrier as an additional empty reasoning fragment. |
| if (text !== undefined && (text.length > 0 || signature === undefined)) { |
| events.push({ |
| kind: 'thinking', |
| text: restoreOpenAiChatEmptyReasoning(text), |
| ...(responsesProviderOptions |
| ? { providerOptions: responsesProviderOptions } |
| : openAiChatReasoningTransportState |
| ? { |
| providerOptions: openAiChatReasoningFieldProviderOptions( |
| openAiChatReasoningTransportState.reasoningField, |
| ), |
| providerOptionsOrigin: 'maka_transport' as const, |
| } |
| : {}), |
| ...(reasoningPartId ? { reasoningPartId } : {}), |
| ...(reasoningSummaryText !== undefined ? { reasoningSummaryText } : {}), |
| }); |
| } |
| return events; |
| } |
| case 'reasoning-end': { |
| const signature = reasoningSignatureFromChunk(chunk); |
| const responsesProviderOptions = runtime |
| ? openAiResponsesReasoningProviderOptionsFromChunk(chunk, runtime) |
| : undefined; |
| const reasoningPartId = reasoningPartIdFromChunk(chunk); |
| const reasoningSummaryText = plaintextSummaryTextFromChunk(chunk, runtime); |
| return [ |
| ...(signature |
| ? [ |
| { |
| kind: 'thinking-signature' as const, |
| signature, |
| ...(reasoningPartId ? { reasoningPartId } : {}), |
| }, |
| ] |
| : []), |
| ...(responsesProviderOptions |
| ? [ |
| { |
| kind: 'thinking' as const, |
| text: '', |
| providerOptions: responsesProviderOptions, |
| ...(reasoningPartId ? { reasoningPartId } : {}), |
| ...(reasoningSummaryText !== undefined ? { reasoningSummaryText } : {}), |
| }, |
| ] |
| : []), |
| ]; |
| } |
| case 'tool-input-start': |
| case 'tool-input-delta': |
| case 'tool-input-end': |
| return [{ kind: 'tool-input', providerExecuted: chunk.providerExecuted === true }]; |
| case 'start-step': |
| case 'tool-result': |
| case 'tool-error': { |
| if ( |
| chunk.providerExecuted !== true || |
| typeof chunk.toolCallId !== 'string' || |
| typeof chunk.toolName !== 'string' |
| ) { |
| return []; |
| } |
| return [ |
| { |
| kind: 'provider-tool-result', |
| toolCallId: chunk.toolCallId, |
| toolName: runtimeToolName?.(chunk.toolName) ?? chunk.toolName, |
| output: chunk.type === 'tool-error' ? chunk.error : (chunk.output ?? chunk.result), |
| ...(chunk.type === 'tool-error' || chunk.isError === true ? { isError: true } : {}), |
| }, |
| ]; |
| } |
| case 'tool-call': { |
| if (typeof chunk.toolCallId !== 'string' || typeof chunk.toolName !== 'string') return []; |
| const toolCall: ToolCallPart = { |
| type: 'tool-call', |
| toolCallId: chunk.toolCallId, |
| toolName: runtimeToolName?.(chunk.toolName) ?? chunk.toolName, |
| input: |
| chunk.providerExecuted === true |
| ? parseProviderExecutedToolInput(chunk.input ?? chunk.args) |
| : (chunk.input ?? chunk.args), |
| ...(chunk.providerExecuted !== undefined |
| ? { providerExecuted: chunk.providerExecuted } |
| : {}), |
| ...(chunk.providerMetadata !== undefined |
| ? { providerOptions: chunk.providerMetadata as ToolCallPart['providerOptions'] } |
| : {}), |
| }; |
| return [{ kind: 'tool-call', toolCall }]; |
| } |
| case 'error': |
| return [{ kind: 'error', failure: normalizeProviderFailure(chunk.error) }]; |
| default: |
| return []; |
| } |
| } |
| |
| /** |
| * OpenAI Responses reserves the provider name `tool_search`. Keep Maka's |
| * persisted/history name intact and translate only the provider-bound copy. |
| */ |
| function remapModelMessageToolNames( |
| messages: readonly ModelMessage[], |
| providerToolName: (name: string) => string, |
| ): ModelMessage[] { |
| const remapContent = <T extends { type: string }>(content: readonly T[]): T[] => |
| content.map((part) => { |
| if ( |
| (part.type === 'tool-call' || part.type === 'tool-result') && |
| 'toolName' in part && |
| typeof part.toolName === 'string' |
| ) { |
| const remapped = { ...part, toolName: providerToolName(part.toolName) } as T & { |
| output?: { type?: string; value?: unknown }; |
| }; |
| if (part.type === 'tool-result' && remapped.output) { |
| if ( |
| (remapped.output.type === 'text' || remapped.output.type === 'error-text') && |
| typeof remapped.output.value === 'string' |
| ) { |
| remapped.output = { |
| ...remapped.output, |
| value: remapProviderToolNamesInText(remapped.output.value, providerToolName), |
| }; |
| } |
| } |
| return remapped as T; |
| } |
| return part; |
| }); |
| |
| return messages.map((message) => { |
| if (message.role === 'assistant' && Array.isArray(message.content)) { |
| return { ...message, content: remapContent(message.content) }; |
| } |
| if (message.role === 'tool') { |
| return { ...message, content: remapContent(message.content) }; |
| } |
| return message; |
| }); |
| } |
| |
| function remapProviderToolNamesInText( |
| text: string, |
| providerToolName: (name: string) => string, |
| ): string { |
| return text.replace(/\btool_search\b/gu, providerToolName(TOOL_SEARCH_NAME)); |
| } |
| |
| function parseProviderExecutedToolInput(input: unknown): unknown { |
| if (typeof input !== 'string') return input; |
| try { |
| return JSON.parse(input); |
| } catch { |
| return input; |
| } |
| } |
| |
| export function lowerModelTools(tools: ModelToolSet): Record<string, unknown> { |
| return Object.fromEntries( |
| Object.entries(tools).map(([name, definition]) => [ |
| name, |
| definition.kind === 'provider' |
| ? compileProviderTool(definition.providerTool) |
| : { |
| ...(definition.description !== undefined |
| ? { description: definition.description } |
| : {}), |
| inputSchema: definition.inputSchema, |
| }, |
| ]), |
| ); |
| } |
| |
| function compileProviderTool( |
| tool: NonNullable<import('./tool-runtime.js').MakaTool['providerTool']>, |
| ): unknown { |
| switch (tool.kind) { |
| case 'openai-apply-patch': |
| return openAiApplyPatchProviderTool; |
| case 'openai-web-search': |
| return openai.tools.webSearch({ |
| ...(tool.searchContextSize ? { searchContextSize: tool.searchContextSize } : {}), |
| }); |
| case 'anthropic-web-search-20250305': |
| return anthropic.tools.webSearch_20250305({ |
| ...(tool.maxUses !== undefined ? { maxUses: tool.maxUses } : {}), |
| }); |
| } |
| } |
| |
| function normalizeProviderFailure(error: unknown): ModelFailure { |
| return isModelFailure(error) ? error : providerModelFailure(error); |
| } |
| |
| function isModelFailure(value: unknown): value is ModelFailure { |
| return ( |
| typeof value === 'object' && |
| value !== null && |
| (value as { type?: unknown }).type === 'model_failure' && |
| typeof (value as { kind?: unknown }).kind === 'string' && |
| typeof (value as { message?: unknown }).message === 'string' |
| ); |
| } |
| |
| type TokenCountBreakdown = { |
| total?: number; |
| noCache?: number; |
| cacheRead?: number; |
| cacheWrite?: number; |
| text?: number; |
| reasoning?: number; |
| }; |
| |
| /** |
| * Internal, adapter-only mirror of the AI SDK raw usage fields. The public |
| * `RawUsageFields` contract lives in `model-protocol.ts`; this stays here as |
| * the lowering input shape and is assigned to `NormalizedUsage.raw`. |
| */ |
| export type AiSdkRawUsageFields = RawUsageFields; |
| |
| export interface AiSdkUsageLike { |
| promptTokens?: number; |
| completionTokens?: number; |
| totalTokens?: number; |
| prompt_tokens?: number; |
| completion_tokens?: number; |
| total_tokens?: number; |
| inputTokens?: number | TokenCountBreakdown; |
| outputTokens?: number | TokenCountBreakdown; |
| cacheHitInputTokens?: number; |
| cacheMissInputTokens?: number; |
| cachedInputTokens?: number; |
| cacheWriteInputTokens?: number; |
| reasoningTokens?: number; |
| cacheReadInputTokens?: number; |
| cacheCreationInputTokens?: number; |
| prompt_cache_hit_tokens?: number; |
| prompt_cache_miss_tokens?: number; |
| prompt_tokens_details?: { |
| cached_tokens?: number; |
| }; |
| completion_tokens_details?: { |
| reasoning_tokens?: number; |
| }; |
| inputTokenDetails?: { |
| cachedTokens?: number; |
| cacheMissTokens?: number; |
| noCacheTokens?: number; |
| cacheReadTokens?: number; |
| cacheWriteTokens?: number; |
| reasoningTokens?: number; |
| }; |
| outputTokenDetails?: { |
| textTokens?: number; |
| reasoningTokens?: number; |
| }; |
| raw?: AiSdkRawUsageFields; |
| } |
| |
| /** |
| * @deprecated alias for the Maka-owned `NormalizedUsage` contract exported |
| * from `model-protocol.ts`. Kept for backward compatibility with existing |
| * internal import sites during the slice-1 transition. |
| */ |
| export type NormalizedAiSdkUsage = NormalizedUsage; |
| |
| export function normalizeAiSdkUsage( |
| usage: AiSdkUsageLike | undefined, |
| options: { rawFinishReason?: unknown } = {}, |
| ): NormalizedUsage | undefined { |
| if (!usage) return undefined; |
| const reportedInputTokens = |
| finiteTokenFromValueOrBreakdown(usage.inputTokens, 'total') ?? |
| finiteTokenBreakdownSum(usage.inputTokens, ['noCache', 'cacheRead', 'cacheWrite']) ?? |
| finiteToken(usage.promptTokens) ?? |
| finiteToken(usage.raw?.prompt_tokens) ?? |
| finiteToken(usage.prompt_tokens) ?? |
| finiteTokenSum([ |
| usage.inputTokenDetails?.noCacheTokens, |
| usage.inputTokenDetails?.cacheReadTokens, |
| usage.inputTokenDetails?.cacheWriteTokens, |
| ]); |
| const reportedOutputTokens = |
| finiteTokenFromValueOrBreakdown(usage.outputTokens, 'total') ?? |
| finiteTokenBreakdownSum(usage.outputTokens, ['text', 'reasoning']) ?? |
| finiteToken(usage.completionTokens) ?? |
| finiteToken(usage.raw?.completion_tokens) ?? |
| finiteToken(usage.completion_tokens) ?? |
| finiteTokenSum([ |
| usage.outputTokenDetails?.textTokens, |
| usage.outputTokenDetails?.reasoningTokens, |
| ]); |
| const reportedCacheHitInputTokens = |
| finiteToken(usage.cacheHitInputTokens) ?? |
| finiteToken(usage.cachedInputTokens) ?? |
| finiteToken(usage.cacheReadInputTokens) ?? |
| finiteToken(usage.raw?.prompt_cache_hit_tokens) ?? |
| finiteToken(usage.prompt_cache_hit_tokens) ?? |
| finiteToken(usage.raw?.prompt_tokens_details?.cached_tokens) ?? |
| finiteToken(usage.prompt_tokens_details?.cached_tokens) ?? |
| finiteTokenFromBreakdown(usage.inputTokens, 'cacheRead') ?? |
| finiteToken(usage.inputTokenDetails?.cacheReadTokens) ?? |
| finiteToken(usage.inputTokenDetails?.cachedTokens); |
| const reportedCacheWriteInputTokens = |
| finiteToken(usage.cacheWriteInputTokens) ?? |
| finiteToken(usage.cacheCreationInputTokens) ?? |
| finiteTokenFromBreakdown(usage.inputTokens, 'cacheWrite') ?? |
| finiteToken(usage.inputTokenDetails?.cacheWriteTokens); |
| const explicitCacheMissInputTokens = |
| finiteToken(usage.cacheMissInputTokens) ?? |
| finiteToken(usage.raw?.prompt_cache_miss_tokens) ?? |
| finiteToken(usage.prompt_cache_miss_tokens) ?? |
| finiteTokenFromBreakdown(usage.inputTokens, 'noCache') ?? |
| finiteToken(usage.inputTokenDetails?.noCacheTokens) ?? |
| finiteToken(usage.inputTokenDetails?.cacheMissTokens); |
| const reportedReasoningTokens = |
| finiteToken(usage.reasoningTokens) ?? |
| finiteTokenFromBreakdown(usage.outputTokens, 'reasoning') ?? |
| finiteToken(usage.outputTokenDetails?.reasoningTokens) ?? |
| finiteToken(usage.raw?.completion_tokens_details?.reasoning_tokens) ?? |
| finiteToken(usage.completion_tokens_details?.reasoning_tokens) ?? |
| finiteToken(usage.inputTokenDetails?.reasoningTokens); |
| const reportedTotalTokens = |
| finiteToken(usage.totalTokens) ?? |
| finiteToken(usage.raw?.total_tokens) ?? |
| finiteToken(usage.total_tokens); |
| const inputTokens = |
| reportedInputTokens ?? |
| (reportedTotalTokens !== undefined && |
| reportedOutputTokens !== undefined && |
| reportedTotalTokens >= reportedOutputTokens |
| ? reportedTotalTokens - reportedOutputTokens |
| : undefined); |
| const outputTokens = |
| reportedOutputTokens ?? |
| (reportedTotalTokens !== undefined && |
| reportedInputTokens !== undefined && |
| reportedTotalTokens >= reportedInputTokens |
| ? reportedTotalTokens - reportedInputTokens |
| : undefined); |
| if (inputTokens === undefined || outputTokens === undefined) return undefined; |
| const cacheHitInputTokens = reportedCacheHitInputTokens ?? 0; |
| const cacheWriteInputTokens = reportedCacheWriteInputTokens ?? 0; |
| const cacheMissInputTokens = |
| explicitCacheMissInputTokens ?? |
| Math.max(0, inputTokens - cacheHitInputTokens - cacheWriteInputTokens); |
| const cacheMissInputSource: CacheMissInputSource = |
| explicitCacheMissInputTokens !== undefined ? 'explicit' : 'derived'; |
| const reasoningTokens = reportedReasoningTokens ?? 0; |
| const totalTokens = reportedTotalTokens ?? inputTokens + outputTokens; |
| const raw = rawUsageFields(usage); |
| const rawFinishReason = rawFinishReasonString(options.rawFinishReason); |
| return { |
| inputTokens, |
| outputTokens, |
| cacheHitInputTokens, |
| cacheMissInputTokens, |
| cacheMissInputSource, |
| cacheWriteInputTokens, |
| reasoningTokens, |
| totalTokens, |
| ...(rawFinishReason !== undefined ? { rawFinishReason } : {}), |
| ...(raw !== undefined ? { raw } : {}), |
| cachedInputTokens: cacheHitInputTokens, |
| }; |
| } |
| |
| function finiteToken(value: unknown): number | undefined { |
| return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined; |
| } |
| |
| function finiteTokenFromBreakdown( |
| value: number | TokenCountBreakdown | undefined, |
| key: keyof TokenCountBreakdown, |
| ): number | undefined { |
| if (!value || typeof value !== 'object') return undefined; |
| return finiteToken(value[key]); |
| } |
| |
| function finiteTokenFromValueOrBreakdown( |
| value: number | TokenCountBreakdown | undefined, |
| key: keyof TokenCountBreakdown, |
| ): number | undefined { |
| return finiteToken(value) ?? finiteTokenFromBreakdown(value, key); |
| } |
| |
| function finiteTokenBreakdownSum( |
| value: number | TokenCountBreakdown | undefined, |
| keys: readonly (keyof TokenCountBreakdown)[], |
| ): number | undefined { |
| if (!value || typeof value !== 'object') return undefined; |
| const parts = keys.map((key) => finiteToken(value[key])); |
| return parts.every((part) => part === undefined) |
| ? undefined |
| : parts.reduce<number>((sum, part) => sum + (part ?? 0), 0); |
| } |
| |
| function finiteTokenSum(values: readonly unknown[]): number | undefined { |
| const tokens = values.map(finiteToken); |
| return tokens.every((token) => token === undefined) |
| ? undefined |
| : tokens.reduce<number>((sum, token) => sum + (token ?? 0), 0); |
| } |
| |
| function rawUsageFields(usage: AiSdkUsageLike): AiSdkRawUsageFields | undefined { |
| const raw: AiSdkRawUsageFields = {}; |
| const promptTokens = finiteToken(usage.prompt_tokens) ?? finiteToken(usage.raw?.prompt_tokens); |
| if (promptTokens !== undefined) raw.prompt_tokens = promptTokens; |
| const completionTokens = |
| finiteToken(usage.completion_tokens) ?? finiteToken(usage.raw?.completion_tokens); |
| if (completionTokens !== undefined) raw.completion_tokens = completionTokens; |
| const totalTokens = finiteToken(usage.total_tokens) ?? finiteToken(usage.raw?.total_tokens); |
| if (totalTokens !== undefined) raw.total_tokens = totalTokens; |
| const promptCacheHitTokens = |
| finiteToken(usage.prompt_cache_hit_tokens) ?? finiteToken(usage.raw?.prompt_cache_hit_tokens); |
| if (promptCacheHitTokens !== undefined) raw.prompt_cache_hit_tokens = promptCacheHitTokens; |
| const promptCacheMissTokens = |
| finiteToken(usage.prompt_cache_miss_tokens) ?? finiteToken(usage.raw?.prompt_cache_miss_tokens); |
| if (promptCacheMissTokens !== undefined) raw.prompt_cache_miss_tokens = promptCacheMissTokens; |
| const cachedTokens = |
| finiteToken(usage.prompt_tokens_details?.cached_tokens) ?? |
| finiteToken(usage.raw?.prompt_tokens_details?.cached_tokens); |
| if (cachedTokens !== undefined) raw.prompt_tokens_details = { cached_tokens: cachedTokens }; |
| const reasoningTokens = |
| finiteToken(usage.completion_tokens_details?.reasoning_tokens) ?? |
| finiteToken(usage.raw?.completion_tokens_details?.reasoning_tokens); |
| if (reasoningTokens !== undefined) { |
| raw.completion_tokens_details = { reasoning_tokens: reasoningTokens }; |
| } |
| return Object.keys(raw).length > 0 ? raw : undefined; |
| } |