mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
refactor(server): drop stale prompt text, duplicated routes and dead options
- lib/system-prompts.ts: delete the extended prompt kept only to reach the 4096-token cache threshold of two old Claude models; describe the current canvas-first UI in the App Features section - app/api/chat/route.ts: remove the server-side example cache (the browser answers examples itself), the ENABLE_HISTORY_XML_REPLACE option and its helper, the unread messageMetadata fields; the DEBUG_LLM_PAYLOAD blocks become one describeMessages summary - app/api/log-feedback and log-save share scoreLatestTrace in lib/langfuse.ts - app/api/admin/test-model and validate-model call a shared testModelConnection() instead of one route faking a request to the other and passing trust through the body - lib/dynamo-quota-manager.ts reads the failing item from the conditional update instead of a second GetItem - lib/ai-providers.ts: geminiThinkingOptions helper for the google and vertexai cases, drop the unused fourth resolveBaseURL parameter - edge-functions: always stream, shrink messageSchema, drop model aliases nothing sends - delete app/api/chat/xml_guide.md, parseOutputTokenLimit, unused type exports and export keywords on file-internal symbols - env.example: drop variables nothing reads, document the quota, validation and analytics variables, mention the apiKeyEnv array form (also in docs/*/ai-providers.md)
This commit is contained in:
@@ -48,8 +48,6 @@ describe("deriveEnvUpdates", () => {
|
||||
expect(updates.ADMIN_OPENAI_API_KEY).toBe("sk-test")
|
||||
expect(updates.OPENAI_API_KEY).toBeUndefined()
|
||||
expect(JSON.parse(updates.ADMIN_PROVIDERS as string)).toHaveLength(1)
|
||||
// AI_MODELS_CONFIG is no longer written (merged at read time)
|
||||
expect(updates.AI_MODELS_CONFIG).toBeNull()
|
||||
})
|
||||
|
||||
it("suffixes env vars for a second instance of the same provider", () => {
|
||||
@@ -137,7 +135,6 @@ describe("deriveEnvUpdates", () => {
|
||||
const prev = [provider()]
|
||||
const updates = deriveEnvUpdates([], prev)
|
||||
expect(updates.ADMIN_OPENAI_API_KEY).toBeNull()
|
||||
expect(updates.AI_MODELS_CONFIG).toBeNull()
|
||||
expect(updates.ADMIN_PROVIDERS).toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
// The request the admin Test hands to validate-model
|
||||
const sent = vi.hoisted(() => ({ body: null as any, headers: null as any }))
|
||||
vi.mock("@/app/api/validate-model/route", () => ({
|
||||
POST: async (req: Request) => {
|
||||
sent.body = await req.json()
|
||||
sent.headers = Object.fromEntries(req.headers)
|
||||
// What the admin Test hands to testModelConnection
|
||||
const sent = vi.hoisted(() => ({ body: null as any, trusted: null as any }))
|
||||
vi.mock("@/lib/test-model-connection", () => ({
|
||||
testModelConnection: async (_req: Request, body: any, trusted: boolean) => {
|
||||
sent.body = body
|
||||
sent.trusted = trusted
|
||||
return Response.json({ valid: true })
|
||||
},
|
||||
}))
|
||||
@@ -61,7 +61,7 @@ describe("admin Test of an entry without a URL", () => {
|
||||
await test({ provider: "openai", apiKey: "panel-key" })
|
||||
expect(sent.body.baseUrl).toBe("https://operator-proxy.example.com/v1")
|
||||
// The server's own URL, tested without the rules for typed URLs
|
||||
expect(sent.body.serverBaseUrl).toBe(true)
|
||||
expect(sent.trusted).toBe(true)
|
||||
|
||||
process.env.AI_GATEWAY_BASE_URL = "https://gateway.example.com/v3/ai"
|
||||
await test({ provider: "gateway", apiKey: "k" })
|
||||
@@ -91,7 +91,7 @@ describe("admin Test of an entry without a URL", () => {
|
||||
"https://vertex-proxy.example.com"
|
||||
await test({ provider: "vertexai", vertexApiKey: "new-key" })
|
||||
expect(sent.body.baseUrl).toBe("https://vertex-proxy.example.com")
|
||||
expect(sent.body.serverBaseUrl).toBe(true)
|
||||
expect(sent.trusted).toBe(true)
|
||||
} finally {
|
||||
delete process.env.GOOGLE_VERTEX_BASE_URL
|
||||
}
|
||||
@@ -103,7 +103,7 @@ describe("admin Test of an entry without a URL", () => {
|
||||
expect(sent.body.baseUrl).toBe(
|
||||
"https://team-openai.openai.azure.com/openai",
|
||||
)
|
||||
expect(sent.body.serverBaseUrl).toBe(true)
|
||||
expect(sent.trusted).toBe(true)
|
||||
})
|
||||
|
||||
it("tests Ollama where chat sends the entry's key", async () => {
|
||||
@@ -111,7 +111,7 @@ describe("admin Test of an entry without a URL", () => {
|
||||
// the SDK's local default (the Test used to go to Ollama Cloud)
|
||||
await test({ provider: "ollama", apiKey: "k" })
|
||||
expect(sent.body.baseUrl).toBe("http://127.0.0.1:11434/api")
|
||||
expect(sent.body.serverBaseUrl).toBe(true)
|
||||
expect(sent.trusted).toBe(true)
|
||||
envFallback.values.OLLAMA_BASE_URL = "http://gpu:11434/api"
|
||||
await test({ provider: "ollama", apiKey: "k" })
|
||||
expect(sent.body.baseUrl).toBe("http://gpu:11434/api")
|
||||
|
||||
@@ -3,7 +3,13 @@ import { generateText } from "ai"
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { getAIModel } from "@/lib/ai-providers"
|
||||
|
||||
const ENV = ["GOOGLE_GENERATIVE_AI_API_KEY", "GOOGLE_TOP_K", "GOOGLE_TOP_P"]
|
||||
const ENV = [
|
||||
"GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
"GOOGLE_TOP_K",
|
||||
"GOOGLE_TOP_P",
|
||||
"GOOGLE_THINKING_BUDGET",
|
||||
"GOOGLE_THINKING_LEVEL",
|
||||
]
|
||||
const saved = Object.fromEntries(ENV.map((k) => [k, process.env[k]]))
|
||||
|
||||
afterEach(() => {
|
||||
@@ -15,7 +21,7 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
/** Send one request through the real Google provider and return its body */
|
||||
async function googleRequestBody() {
|
||||
async function googleRequestBody(modelId = "gemini-2.5-flash") {
|
||||
let body: any
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
@@ -34,11 +40,17 @@ async function googleRequestBody() {
|
||||
)
|
||||
}),
|
||||
)
|
||||
const { model } = getAIModel({
|
||||
// providerOptions carry the thinking settings, as the chat route passes them
|
||||
const { model, providerOptions } = getAIModel({
|
||||
provider: "google",
|
||||
modelId: "gemini-2.5-flash",
|
||||
modelId,
|
||||
})
|
||||
await generateText({
|
||||
model,
|
||||
prompt: "hi",
|
||||
maxRetries: 0,
|
||||
...(providerOptions && { providerOptions }),
|
||||
})
|
||||
await generateText({ model, prompt: "hi", maxRetries: 0 })
|
||||
return body
|
||||
}
|
||||
|
||||
@@ -60,3 +72,27 @@ describe("Google sampling settings", () => {
|
||||
expect(body.generationConfig?.topP).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Gemini thinking settings", () => {
|
||||
it("sends GOOGLE_THINKING_BUDGET as the Gemini 2.5 thinking budget", async () => {
|
||||
process.env.GOOGLE_GENERATIVE_AI_API_KEY = "test-key"
|
||||
process.env.GOOGLE_THINKING_BUDGET = "8192"
|
||||
delete process.env.GOOGLE_THINKING_LEVEL
|
||||
const body = await googleRequestBody("gemini-2.5-flash")
|
||||
expect(body.generationConfig.thinkingConfig).toEqual({
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 8192,
|
||||
})
|
||||
})
|
||||
|
||||
it("sends GOOGLE_THINKING_LEVEL as the Gemini 3 thinking level", async () => {
|
||||
process.env.GOOGLE_GENERATIVE_AI_API_KEY = "test-key"
|
||||
process.env.GOOGLE_THINKING_LEVEL = "high"
|
||||
delete process.env.GOOGLE_THINKING_BUDGET
|
||||
const body = await googleRequestBody("gemini-3-pro-preview")
|
||||
expect(body.generationConfig.thinkingConfig).toEqual({
|
||||
includeThoughts: true,
|
||||
thinkingLevel: "high",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
describe("resolveBaseURL", () => {
|
||||
const SERVER_BASE_URL = "https://server-proxy.example.com"
|
||||
const USER_BASE_URL = "https://user-proxy.example.com"
|
||||
const DEFAULT_BASE_URL = "https://api.provider.com/v1"
|
||||
const USER_API_KEY = "user-api-key-123"
|
||||
|
||||
describe("when user provides their own API key", () => {
|
||||
@@ -18,28 +17,12 @@ describe("resolveBaseURL", () => {
|
||||
USER_API_KEY,
|
||||
USER_BASE_URL,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
expect(result).toBe(USER_BASE_URL)
|
||||
})
|
||||
|
||||
it("uses default baseUrl when user provides no baseUrl", () => {
|
||||
const result = resolveBaseURL(
|
||||
USER_API_KEY,
|
||||
null,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
expect(result).toBe(DEFAULT_BASE_URL)
|
||||
})
|
||||
|
||||
it("returns undefined when user provides no baseUrl and no default exists", () => {
|
||||
const result = resolveBaseURL(
|
||||
USER_API_KEY,
|
||||
null,
|
||||
SERVER_BASE_URL,
|
||||
undefined,
|
||||
)
|
||||
it("returns undefined when user provides no baseUrl", () => {
|
||||
const result = resolveBaseURL(USER_API_KEY, null, SERVER_BASE_URL)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -48,92 +31,47 @@ describe("resolveBaseURL", () => {
|
||||
USER_API_KEY,
|
||||
undefined,
|
||||
SERVER_BASE_URL,
|
||||
undefined,
|
||||
)
|
||||
// Should NOT return SERVER_BASE_URL
|
||||
expect(result).not.toBe(SERVER_BASE_URL)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("prefers user's baseUrl over default", () => {
|
||||
const result = resolveBaseURL(
|
||||
USER_API_KEY,
|
||||
USER_BASE_URL,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
expect(result).toBe(USER_BASE_URL)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when using server credentials (no user API key)", () => {
|
||||
it("uses user's baseUrl when provided (overrides server)", () => {
|
||||
const result = resolveBaseURL(
|
||||
null,
|
||||
USER_BASE_URL,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
const result = resolveBaseURL(null, USER_BASE_URL, SERVER_BASE_URL)
|
||||
expect(result).toBe(USER_BASE_URL)
|
||||
})
|
||||
|
||||
it("falls back to server's baseUrl when no user baseUrl", () => {
|
||||
const result = resolveBaseURL(
|
||||
null,
|
||||
null,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
const result = resolveBaseURL(null, null, SERVER_BASE_URL)
|
||||
expect(result).toBe(SERVER_BASE_URL)
|
||||
})
|
||||
|
||||
it("falls back to default when no user or server baseUrl", () => {
|
||||
const result = resolveBaseURL(
|
||||
null,
|
||||
null,
|
||||
undefined,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
expect(result).toBe(DEFAULT_BASE_URL)
|
||||
})
|
||||
|
||||
it("returns undefined when no baseUrl available anywhere", () => {
|
||||
const result = resolveBaseURL(null, null, undefined, undefined)
|
||||
const result = resolveBaseURL(null, null, undefined)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
|
||||
it("handles undefined apiKey same as null", () => {
|
||||
const result = resolveBaseURL(
|
||||
undefined,
|
||||
null,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
const result = resolveBaseURL(undefined, null, SERVER_BASE_URL)
|
||||
expect(result).toBe(SERVER_BASE_URL)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles empty string apiKey as falsy (uses server config)", () => {
|
||||
const result = resolveBaseURL(
|
||||
"",
|
||||
null,
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
const result = resolveBaseURL("", null, SERVER_BASE_URL)
|
||||
// Empty string is falsy, so should use server config
|
||||
expect(result).toBe(SERVER_BASE_URL)
|
||||
})
|
||||
|
||||
it("handles empty string baseUrl as falsy", () => {
|
||||
const result = resolveBaseURL(
|
||||
USER_API_KEY,
|
||||
"",
|
||||
SERVER_BASE_URL,
|
||||
DEFAULT_BASE_URL,
|
||||
)
|
||||
// Empty string baseUrl is falsy, should fall back to default
|
||||
expect(result).toBe(DEFAULT_BASE_URL)
|
||||
const result = resolveBaseURL(USER_API_KEY, "", SERVER_BASE_URL)
|
||||
// Empty string baseUrl is falsy; a user's key never goes to the
|
||||
// server's URL, so nothing is left
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
dropInvalidToolCalls,
|
||||
fixToolInputJson,
|
||||
replaceHistoricalToolInputs,
|
||||
validateFileParts,
|
||||
} from "@/lib/chat-helpers"
|
||||
|
||||
@@ -94,91 +93,6 @@ describe("validateFileParts", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("replaceHistoricalToolInputs", () => {
|
||||
it("replaces display_diagram tool inputs with placeholder", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolName: "display_diagram",
|
||||
input: { xml: "<mxCell...>" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = replaceHistoricalToolInputs(messages)
|
||||
expect(result[0].content[0].input.placeholder).toContain(
|
||||
"XML content replaced",
|
||||
)
|
||||
})
|
||||
|
||||
it("replaces edit_diagram tool inputs with placeholder", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolName: "edit_diagram",
|
||||
input: { operations: [] },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = replaceHistoricalToolInputs(messages)
|
||||
expect(result[0].content[0].input.placeholder).toContain(
|
||||
"XML content replaced",
|
||||
)
|
||||
})
|
||||
|
||||
it("leaves tool calls with invalid inputs for dropInvalidToolCalls", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolName: "display_diagram",
|
||||
input: {},
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
toolName: "display_diagram",
|
||||
input: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = replaceHistoricalToolInputs(messages)
|
||||
expect(result[0].content).toEqual(messages[0].content)
|
||||
})
|
||||
|
||||
it("preserves non-assistant messages", () => {
|
||||
const messages = [{ role: "user", content: "hello" }]
|
||||
const result = replaceHistoricalToolInputs(messages)
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("preserves other tool calls", () => {
|
||||
const messages = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolName: "other_tool",
|
||||
input: { foo: "bar" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const result = replaceHistoricalToolInputs(messages)
|
||||
expect(result[0].content[0].input).toEqual({ foo: "bar" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("dropInvalidToolCalls", () => {
|
||||
it("drops an invalid tool-call together with its tool-result", () => {
|
||||
const messages = [
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { onRequest } from "@/edge-functions/api/edgeai/chat/completions"
|
||||
|
||||
// EdgeOne's global AI object, answering with an empty stream
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("AI", {
|
||||
chatCompletions: async () => new ReadableStream(),
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function request(headers: Record<string, string>): Request {
|
||||
return new Request("http://localhost/api/edgeai/chat/completions", {
|
||||
method: "POST",
|
||||
headers,
|
||||
// Non-streaming requests return a mock reply without calling AI
|
||||
body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] }),
|
||||
})
|
||||
}
|
||||
|
||||
const json = { "Content-Type": "application/json" }
|
||||
|
||||
// An accepted request streams the model's reply
|
||||
function expectStream(res: Response) {
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get("content-type")).toContain("text/event-stream")
|
||||
}
|
||||
|
||||
describe("EdgeOne chat completions function", () => {
|
||||
it("sends no CORS headers", async () => {
|
||||
const res = await onRequest({ request: request(json), env: {} })
|
||||
expect(res.status).toBe(200)
|
||||
expectStream(res)
|
||||
expect(res.headers.get("access-control-allow-origin")).toBeNull()
|
||||
})
|
||||
|
||||
@@ -40,7 +55,7 @@ describe("EdgeOne chat completions function", () => {
|
||||
}),
|
||||
env: {},
|
||||
})
|
||||
expect(withCharset.status).toBe(200)
|
||||
expectStream(withCharset)
|
||||
})
|
||||
|
||||
it("checks the access code when ACCESS_CODE_LIST is set", async () => {
|
||||
@@ -52,11 +67,11 @@ describe("EdgeOne chat completions function", () => {
|
||||
request: request({ ...json, "x-access-code": "secret" }),
|
||||
env,
|
||||
})
|
||||
expect(ok.status).toBe(200)
|
||||
expectStream(ok)
|
||||
})
|
||||
|
||||
it("lets requests through when env is unavailable", async () => {
|
||||
const res = await onRequest({ request: request(json) })
|
||||
expect(res.status).toBe(200)
|
||||
expectStream(res)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { POST as logSave } from "@/app/api/log-save/route"
|
||||
|
||||
// A Langfuse client with a trace to attach the save score to
|
||||
// A Langfuse client with a trace to attach the save score to; the real
|
||||
// scoreLatestTrace runs against it
|
||||
const batch = vi.fn(async () => ({}))
|
||||
vi.mock("@/lib/langfuse", () => ({
|
||||
vi.mock("@/lib/langfuse", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@/lib/langfuse")>()),
|
||||
getLangfuseClient: () => ({
|
||||
api: {
|
||||
trace: { list: async () => ({ data: [{ id: "trace-1" }] }) },
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
DEFAULT_MAX_OUTPUT_TOKENS,
|
||||
parseOutputTokenLimit,
|
||||
readCeiling,
|
||||
rejectionText,
|
||||
resolveMaxOutputTokens,
|
||||
retryOutputTokens,
|
||||
withOutputTokenLimitFallback,
|
||||
} from "@/lib/output-token-limit"
|
||||
|
||||
describe("parseOutputTokenLimit", () => {
|
||||
// The ceiling a rejection names, before the "too small to retry" check
|
||||
const parse = (error: unknown) => {
|
||||
const text = rejectionText(error)
|
||||
return text ? readCeiling(text) : null
|
||||
}
|
||||
|
||||
describe("readCeiling", () => {
|
||||
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)
|
||||
expect(parse(error)).toBe(4096)
|
||||
})
|
||||
|
||||
it("subtracts the input when the ceiling covers input plus output", () => {
|
||||
@@ -22,7 +29,7 @@ describe("parseOutputTokenLimit", () => {
|
||||
"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)
|
||||
expect(parse(error)).toBe(62951)
|
||||
})
|
||||
|
||||
it("reads the ceiling from an Anthropic rejection", () => {
|
||||
@@ -30,7 +37,7 @@ describe("parseOutputTokenLimit", () => {
|
||||
message:
|
||||
"max_tokens: 200000 > 64000, which is the maximum allowed number of output tokens for claude-sonnet-4-5",
|
||||
}
|
||||
expect(parseOutputTokenLimit(error)).toBe(64000)
|
||||
expect(parse(error)).toBe(64000)
|
||||
})
|
||||
|
||||
it("reads the ceiling from an OpenAI rejection", () => {
|
||||
@@ -38,7 +45,7 @@ describe("parseOutputTokenLimit", () => {
|
||||
message:
|
||||
"max_tokens is too large: 64000. This model supports at most 16384 completion tokens",
|
||||
}
|
||||
expect(parseOutputTokenLimit(error)).toBe(16384)
|
||||
expect(parse(error)).toBe(16384)
|
||||
})
|
||||
|
||||
it("looks in the response body too", () => {
|
||||
@@ -46,25 +53,25 @@ describe("parseOutputTokenLimit", () => {
|
||||
message: "Bad request",
|
||||
responseBody: '{"message":"exceeds the model limit of 10000."}',
|
||||
}
|
||||
expect(parseOutputTokenLimit(error)).toBe(10000)
|
||||
expect(parse(error)).toBe(10000)
|
||||
})
|
||||
|
||||
it("returns null for unrelated errors", () => {
|
||||
expect(parseOutputTokenLimit({ message: "Invalid API key" })).toBeNull()
|
||||
expect(parseOutputTokenLimit(undefined)).toBeNull()
|
||||
expect(parse({ message: "Invalid API key" })).toBeNull()
|
||||
expect(parse(undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it("ignores a number that is not about tokens", () => {
|
||||
// An earlier draft matched "lower than N" generically, which turned any
|
||||
// message shaped like this into a bogus budget
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message: "temperature must be lower than 2",
|
||||
statusCode: 400,
|
||||
}),
|
||||
).toBeNull()
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message: "reduce requests to lower than 60 per minute",
|
||||
statusCode: 429,
|
||||
}),
|
||||
@@ -76,28 +83,26 @@ describe("parseOutputTokenLimit", () => {
|
||||
message: "exceeds the model limit of 4096",
|
||||
statusCode: 429,
|
||||
}
|
||||
expect(parseOutputTokenLimit(error)).toBeNull()
|
||||
expect(parse(error)).toBeNull()
|
||||
})
|
||||
|
||||
it("rejects a ceiling too small to hold a diagram", () => {
|
||||
expect(
|
||||
parseOutputTokenLimit({ message: "model limit of 200" }),
|
||||
).toBeNull()
|
||||
it("reads a ceiling too small to hold a diagram as it is", () => {
|
||||
// retryOutputTokens is what refuses to retry on these
|
||||
expect(parse({ message: "model limit of 200" })).toBe(200)
|
||||
// Context ceiling that leaves almost nothing after the input
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message:
|
||||
"This endpoint's maximum context length is 64000 tokens. However, you requested about 128000 tokens (63500 of text input, 64000 in the output).",
|
||||
}),
|
||||
).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()
|
||||
).toBe(64000 - 63500 - 1024)
|
||||
// The input alone fills the context
|
||||
expect(
|
||||
parse({
|
||||
message:
|
||||
"This endpoint's maximum context length is 1000 tokens. However, you requested about 65000 tokens (64000 of text input, 1000 in the output).",
|
||||
}),
|
||||
).toBe(1000 - 64000 - 1024)
|
||||
})
|
||||
|
||||
it("reads the ceiling from a Volcengine Ark rejection", () => {
|
||||
@@ -106,10 +111,10 @@ describe("parseOutputTokenLimit", () => {
|
||||
"The parameter `max_tokens` specified in the request are not valid: integer above maximum value, expected a value <= 32768, but got 64000 instead.",
|
||||
statusCode: 400,
|
||||
}
|
||||
expect(parseOutputTokenLimit(error)).toBe(32768)
|
||||
expect(parse(error)).toBe(32768)
|
||||
// Same message JSON-escaped in the response body
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message: "Bad request",
|
||||
responseBody:
|
||||
'{"error":{"message":"The parameter `max_tokens` specified in the request are not valid: integer above maximum value, expected a value \\u003c= 16384, but got 64000 instead."}}',
|
||||
@@ -122,27 +127,27 @@ describe("parseOutputTokenLimit", () => {
|
||||
message:
|
||||
"<400> InternalError.Algo.InvalidParameter: Range of max_tokens should be [1, 8192]",
|
||||
}
|
||||
expect(parseOutputTokenLimit(error)).toBe(8192)
|
||||
expect(parse(error)).toBe(8192)
|
||||
})
|
||||
|
||||
it("subtracts the input in SGLang and vLLM context rejections", () => {
|
||||
// SGLang
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message:
|
||||
"Requested token count exceeds the model's maximum context length of 32768 tokens. You requested a total of 70000 tokens: 6000 tokens from the input messages and 64000 tokens for the completion.",
|
||||
}),
|
||||
).toBe(32768 - 6000 - 1024)
|
||||
// vLLM, older wording
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message:
|
||||
"This model's maximum context length is 32768 tokens. However, you requested 70000 tokens (6000 in the messages, 64000 in the completion).",
|
||||
}),
|
||||
).toBe(32768 - 6000 - 1024)
|
||||
// vLLM, newer wording
|
||||
expect(
|
||||
parseOutputTokenLimit({
|
||||
parse({
|
||||
message:
|
||||
"This model's maximum context length is 32768 tokens and your request has 6000 input tokens (64000 > 32768 - 6000).",
|
||||
}),
|
||||
|
||||
@@ -257,17 +257,37 @@ describe("testing EdgeOne", () => {
|
||||
})
|
||||
|
||||
describe("the admin Test of the server's own base URL", () => {
|
||||
const test = (headers: Record<string, string>) =>
|
||||
// An admin panel entry without a URL of its own
|
||||
const adminTest = () =>
|
||||
testModel(
|
||||
new Request("http://localhost/api/admin/test-model", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-admin-password": "admin-pw",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: {
|
||||
id: "p1",
|
||||
provider: "openai",
|
||||
apiKey: "panel-key",
|
||||
models: ["gpt-5.5"],
|
||||
},
|
||||
modelId: "gpt-5.5",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
// A user naming the same address in the Settings dialog
|
||||
const userTest = () =>
|
||||
validateModel(
|
||||
new Request("http://localhost/api/validate-model", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...headers },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
provider: "openai",
|
||||
apiKey: "panel-key",
|
||||
modelId: "gpt-5.5",
|
||||
baseUrl: "http://10.0.0.5:8000/v1",
|
||||
serverBaseUrl: true,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
@@ -280,12 +300,10 @@ describe("the admin Test of the server's own base URL", () => {
|
||||
privateUrls.all = true
|
||||
try {
|
||||
streamReply({ role: "assistant", content: "OK" })
|
||||
const admin = await (
|
||||
await test({ "x-admin-password": "admin-pw" })
|
||||
).json()
|
||||
const admin = await (await adminTest()).json()
|
||||
expect(admin.valid).toBe(true)
|
||||
// Anyone else claiming it is still refused
|
||||
const other = await test({})
|
||||
// A user typing the same address is still refused
|
||||
const other = await userTest()
|
||||
expect(other.status).toBe(400)
|
||||
} finally {
|
||||
privateUrls.all = false
|
||||
|
||||
Reference in New Issue
Block a user