mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
A reasoning model spends the output budget in order: thinking first, then prose, then the tool call. With 16000 the thinking alone can consume all of it, so the turn ends with finishReason "length" before display_diagram is ever called. The canvas stays empty and nothing surfaces in the UI, because no tool call means no tool error, and the client never reads finishReason. Measured on openrouter deepseek/deepseek-v4-flash, the model from the report: - max_tokens=800 with reasoning on returns reasoning_tokens=800, empty content, finish_reason length. So reasoning is billed against this budget, not exempt. - refining an existing diagram (19k chars of XML in the input) produced 49142 chars of reasoning, zero tool calls, finishReason "length" at 16000 - the same request at 40000 finished and called edit_diagram with 12 operations 64000 cannot just be sent to every model: bedrock claude-3-haiku caps at 4096, nova-lite at 10000, and the openrouter deepseek-r1 endpoint counts input and output against one 64000 ceiling. All three name the real limit in the 400, so parse it and retry once. Verified: nova-lite logs "64000 rejected, retrying with 10000" and then completes its tool call. Also expose the budget in Settings. It is sent as a header rather than read from env only, so desktop users can raise it themselves without an env file. vercel.json goes back to the 300s it had before #238 traded it for $2-4/month. That is now Vercel's own default, and billing pauses while the function waits on the model, so the saving that motivated 120s no longer applies. edgeone.json is left alone: its 120 may be that platform's actual ceiling.
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
import {
|
|
DEFAULT_MAX_OUTPUT_TOKENS,
|
|
parseOutputTokenLimit,
|
|
resolveMaxOutputTokens,
|
|
} from "@/lib/output-token-limit"
|
|
|
|
describe("parseOutputTokenLimit", () => {
|
|
it("reads the ceiling from a Bedrock rejection", () => {
|
|
const error = {
|
|
message:
|
|
"The maximum tokens you requested exceeds the model limit of 4096. Try again with a maximum tokens value that is lower than 4096.",
|
|
}
|
|
expect(parseOutputTokenLimit(error)).toBe(4096)
|
|
})
|
|
|
|
it("subtracts the input when the ceiling covers input plus output", () => {
|
|
const error = {
|
|
message:
|
|
"This endpoint's maximum context length is 64000 tokens. However, you requested about 64025 tokens (25 of text input, 64000 in the output).",
|
|
}
|
|
// 64000 - 25 - 1024 margin
|
|
expect(parseOutputTokenLimit(error)).toBe(62951)
|
|
})
|
|
|
|
it("reads the ceiling from an Anthropic rejection", () => {
|
|
const error = {
|
|
message:
|
|
"max_tokens: 200000 > 64000, which is the maximum allowed number of output tokens for claude-sonnet-4-5",
|
|
}
|
|
expect(parseOutputTokenLimit(error)).toBe(64000)
|
|
})
|
|
|
|
it("reads the ceiling from an OpenAI rejection", () => {
|
|
const error = {
|
|
message:
|
|
"max_tokens is too large: 64000. This model supports at most 16384 completion tokens",
|
|
}
|
|
expect(parseOutputTokenLimit(error)).toBe(16384)
|
|
})
|
|
|
|
it("looks in the response body too", () => {
|
|
const error = {
|
|
message: "Bad request",
|
|
responseBody: '{"message":"exceeds the model limit of 10000."}',
|
|
}
|
|
expect(parseOutputTokenLimit(error)).toBe(10000)
|
|
})
|
|
|
|
it("returns null for unrelated errors", () => {
|
|
expect(parseOutputTokenLimit({ message: "Invalid API key" })).toBeNull()
|
|
expect(parseOutputTokenLimit(undefined)).toBeNull()
|
|
})
|
|
|
|
it("returns null when the input alone fills the context", () => {
|
|
const error = {
|
|
message:
|
|
"This endpoint's maximum context length is 1000 tokens. However, you requested about 65000 tokens (64000 of text input, 1000 in the output).",
|
|
}
|
|
expect(parseOutputTokenLimit(error)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("resolveMaxOutputTokens", () => {
|
|
it("uses a valid header value", () => {
|
|
expect(resolveMaxOutputTokens("32000")).toBe(32000)
|
|
})
|
|
|
|
it("falls back to the default for missing or bogus values", () => {
|
|
expect(resolveMaxOutputTokens(null)).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
expect(resolveMaxOutputTokens("")).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
expect(resolveMaxOutputTokens("abc")).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
expect(resolveMaxOutputTokens("0")).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
expect(resolveMaxOutputTokens("-5")).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
expect(resolveMaxOutputTokens("1.5")).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
// Above the sanity ceiling, e.g. an extra zero
|
|
expect(resolveMaxOutputTokens("640000")).toBe(DEFAULT_MAX_OUTPUT_TOKENS)
|
|
})
|
|
})
|