diff --git a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts index 5a156ea281..8af6b75569 100644 --- a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts +++ b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts @@ -81,6 +81,39 @@ describe('AiSdkBackend model history', () => { ]); }); + test('sends the selected Kimi model output limit instead of the Anthropic unknown-model default', async () => { + const model = completionModel(); + const backend = new AiSdkBackend({ + sessionId: 'session-1', + header: header(), + appendMessage: async () => {}, + connection: { + slug: 'kimi-coding-plan', + name: 'Kimi Coding Plan', + providerType: 'kimi-coding-plan', + defaultModel: 'k3', + enabled: true, + createdAt: 1, + updatedAt: 1, + }, + apiKey: 'sk-test', + modelId: 'k3', + permissionEngine: new PermissionEngine({ newId: () => 'permission-id', now: () => 1 }), + modelFactory: () => model, + tools: [], + newId: idGenerator(), + now: monotonicClock(), + }); + + await drain(backend.send({ + turnId: 'turn-current', + text: 'current user', + context: [], + })); + + assert.equal(model.doStreamCalls[0]?.maxOutputTokens, 131_072); + }); + test('prefers RuntimeEvent prior messages and appends current user once', async () => { const model = completionModel(); const backend = new AiSdkBackend({ diff --git a/packages/runtime/src/model-adapter.ts b/packages/runtime/src/model-adapter.ts index f4bfc07c3f..8919269e99 100644 --- a/packages/runtime/src/model-adapter.ts +++ b/packages/runtime/src/model-adapter.ts @@ -6,6 +6,7 @@ import type { CompleteEvent, } from '@maka/core/events'; import { providerAuthRequiresSecret, type LlmConnection } from '@maka/core/llm-connections'; +import { lookupModelMetadata } from '@maka/core/model-metadata'; import { generalizedErrorMessage } from '@maka/core/redaction'; import type { CacheMissInputSource } from '@maka/core/usage-stats/types'; import type { ModelMessage } from 'ai'; @@ -180,6 +181,7 @@ export class ModelAdapter { }; const maxSteps = input.maxSteps ?? this.input.maxSteps; + const maxOutputTokens = selectedModelMaxOutputTokens(this.input.connection, this.input.modelId); const configuredStop = maxSteps === undefined ? isLoopFinished() : isStepCount(maxSteps); @@ -192,6 +194,7 @@ export class ModelAdapter { ...(input.prepareStep ? { prepareStep: input.prepareStep } : {}), repairToolCall: input.repairToolCall, ...(input.system ? { instructions: input.system } : {}), + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), providerOptions: this.input.providerOptions, // streamText defaults to one step when stopWhen is omitted. Its exported // non-stopping condition is required for an unbounded tool loop. @@ -347,6 +350,11 @@ export class ModelAdapter { } } +function selectedModelMaxOutputTokens(connection: LlmConnection, modelId: string): number | undefined { + return connection.models?.find((model) => model.id === modelId)?.maxOutputTokens + ?? lookupModelMetadata(connection.providerType, modelId).maxOutputTokens; +} + export interface ModelAdapterRuntimeEventReplaySupport { toolCalls: boolean; toolResults: boolean;