openvibers/openvibe.ai Commits
DeepSeek Flash: give reasoning room, score empty attempts as failures (#354)
Changes
@@@ -11,9 +11,10 @@** Uptime is per real provider attempt, not per run status. A failed run counts against an offer only* when an attempt row names that provider and model; a run refused before routing, over quota or by-- * validation has no such row and counts against nothing. An attempt that answered (status ok) is a-- * success even when the run afterwards failed validation (`output.empty` / `output.invalid`): the-- * offer did answer. A skipped or cancelled row was never an answer and is not a sample.++ * validation has no such row and counts against nothing. An attempt is a success only when it produced++ * output: an ok row whose run ended `output.empty`, or a text attempt that spent every output token++ * (tokens_out = 0, e.g. all of max_tokens went to reasoning), is a failure of that provider and model.++ * A skipped or cancelled row was never an answer and is not a sample.** The latency of an answer is its time to the first token when the receipt recorded one (a streamed* answer), else the total provider latency of the attempt that answered. Uptime and speed need enough@@@ -23,6 +24,8 @@*/const DAY_MS = 86400000;const MIN_SAMPLES = 20;++// sha256(''): what the router stores as a request's output_hash when the provider produced no content.++const EMPTY_OUTPUT_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';const dayOf = (ms) => new Date(ms).toISOString().slice(0, 10);// What an upstream appends to a registry name: -2025-08-07, -v3-0324, -4-5-20251001, :free, @rev.@@@ -60,11 +63,19 @@ function percentile(values, p) {}function createObserved({ db, clock = { now: () => Date.now() } }) {-- // One sample per real provider attempt: ok is a success, error or timeout is a failure. Skipped and-- // cancelled rows are not samples (no provider was tried, or the caller left). The run's own provider-- // and model are not used here: a fallback run can name several offers, and a failed run keeps none.++ // One sample per real provider attempt: ok with output is a success; error, timeout, an empty run++ // and a text attempt that spent every token are failures. Skipped and cancelled rows are not samples++ // (no provider was tried, or the caller left). The run's own provider and model are not used here: a++ // fallback run can name several offers, and a failed run keeps none.const attemptRows = db.prepare(`SELECT to_char(r.created_at::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,-- CASE q.status WHEN 'ok' THEN 'succeeded' ELSE 'failed' END AS status,++ CASE++ WHEN q.status <> 'ok' THEN 'failed'++ WHEN r.error_code = 'output.empty' THEN 'failed'++ WHEN q.operation IN ('chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages')++ AND COALESCE(q.tokens_out, 0) = 0++ AND (q.output_hash IS NULL OR q.output_hash = '${EMPTY_OUTPUT_HASH}') THEN 'failed'++ ELSE 'succeeded'++ END AS status,q.provider_key AS provider_key, q.model_key AS model_key,COUNT(*)::bigint AS nFROM requests q@@@ -92,6 +103,10 @@ function createObserved({ db, clock = { now: () => Date.now() } }) {LEFT JOIN ai_receipts rc ON rc.run_id = r.idWHERE q.status = 'ok' AND q.latency_ms IS NOT NULLAND r.status IN ('succeeded', 'failed')++ AND (r.error_code IS NULL OR r.error_code <> 'output.empty')++ AND NOT (q.operation IN ('chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages')++ AND COALESCE(q.tokens_out, 0) = 0++ AND (q.output_hash IS NULL OR q.output_hash = '${EMPTY_OUTPUT_HASH}'))AND r.created_at::timestamptz >= ?::timestamptz AND r.created_at::timestamptz <= ?::timestamptz`);/** The real attempts and cached answers of the window, and the UTC day that is "today". */@@@ -1385,7 +1385,16 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.noif (uncensored) { attemptFacts.content_policy = 'uncensored'; attemptFacts.label = tier.LABEL; }// The hold needs the offer's retail card, whether it is prepaid, and the real output cap.const attemptRow = p && model ? await registry.getModel(p.key, model) : null;-- const attemptMeta = { provider: p ? p.key : null, model, maxTokens: req.maxTokens, paid: !isFreeProvider(p), retail: modelCatalog.retailRate(attemptRow) };++ // A reasoning model spends the same max_tokens allowance thinking before it writes: a caller's++ // 300-token cap can be all reasoning, leaving content empty (finish_reason 'length', output 0).++ // The registry says how much room this model needs on top of the answer the caller asked for.++ const reserve = Number(attemptRow && attemptRow.metadata && attemptRow.metadata.reasoning_reserve_tokens) || 0;++ const requestedMax = Number(req.maxTokens);++ const modelMax = reserve > 0 && Number.isSafeInteger(requestedMax) && requestedMax >= 1++ && ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages'].includes(operation)++ ? requestedMax + reserve++ : req.maxTokens;++ const attemptMeta = { provider: p ? p.key : null, model, maxTokens: modelMax, paid: !isFreeProvider(p), retail: modelCatalog.retailRate(attemptRow) };const routed = typeof ctx.onRoute === 'function' ? await ctx.onRoute(attemptFacts, attemptMeta) : null;if (routed && routed.skip) {statFor(c.provider).skipped++;@@@ -1395,8 +1404,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.nocontinue candidate;}// max_cost_usd, when it binds, comes back as the affordable output cap for this attempt.-- const callMax = routed && Number.isSafeInteger(routed.maxTokens) && routed.maxTokens >= 1 && routed.maxTokens !== req.maxTokens ? routed.maxTokens : null;-- let callReq = callMax == null ? req : { ...req, maxTokens: callMax };++ const callMax = routed && Number.isSafeInteger(routed.maxTokens) && routed.maxTokens >= 1 && routed.maxTokens !== modelMax ? routed.maxTokens : null;++ let callReq = modelMax !== req.maxTokens ? { ...req, maxTokens: modelMax } : req;++ if (callMax != null) callReq = { ...callReq, maxTokens: callMax };// The host's model writes a short answer: its own cap, never more, whatever the route asked for.if (local && operation !== 'embed') {const localMax = Math.max(1, (config.localLlm && config.localLlm.maxTokens) || 512);@@@ -35,14 +35,14 @@ const DEEPSEEK_MODELS = [context_window: 1048576, max_output: 393216,supports: { json: true, tools: true, streaming: true, vision: true },cost: { in_per_mtok: 0.30, out_per_mtok: 1.20, cached_per_mtok: 0.006, source: 'https://api-docs.deepseek.com/quick_start/pricing', verified_at: '2026-11-06' },-- metadata: { org: 'deepseek', model: 'deepseek-flash', family: 'DeepSeek', tier: 'fast', reasoning: true, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.15 in, $0.003 cached, $0.60 out per 1M.' },++ metadata: { org: 'deepseek', model: 'deepseek-flash', family: 'DeepSeek', tier: 'fast', reasoning: true, reasoning_reserve_tokens: 1536, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.15 in, $0.003 cached, $0.60 out per 1M.' },},{model_key: 'deepseek-v4-pro', display_name: 'DeepSeek V4 Pro', type: 'chat',context_window: 1048576, max_output: 393216,supports: { json: true, tools: true, streaming: true, vision: false },cost: { in_per_mtok: 1.32, out_per_mtok: 3.96, cached_per_mtok: 0.044, source: 'https://api-docs.deepseek.com/quick_start/pricing', verified_at: '2026-11-06' },-- metadata: { org: 'deepseek', model: 'deepseek-v4-pro', family: 'DeepSeek', tier: 'strong', reasoning: true, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.66 in, $0.022 cached, $1.98 out per 1M.' },++ metadata: { org: 'deepseek', model: 'deepseek-v4-pro', family: 'DeepSeek', tier: 'strong', reasoning: true, reasoning_reserve_tokens: 1536, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.66 in, $0.022 cached, $1.98 out per 1M.' },},];nnew file mode 100644@@@ -0,0 +1,93 @@++'use strict';++// DeepSeek V4.1 Flash is a reasoning model: `max_tokens` is the whole allowance, and reasoning is spent++// from it before content. A 48-token cap can therefore be all thinking, finish_reason 'length', content++// empty and usage output 0. The real response (2026-11-07) keeps the answer in choices[0].message.content++// and the thinking in reasoning_content; usage counts reasoning_tokens inside completion_tokens. The++// adapter must read content, and the registry says how much room this model needs beyond the caller's cap.++const assert = require('assert');++const fixture = require('./fixtures/deepseek-flash.json');++const { boot, suite, seamServer } = require('./helpers');++const { createOpenAiProvider } = require('../server/providers/openai');++++const t = suite('deepseek-flash');++const completion = fixture.completion;++const answer = completion.choices[0].message.content;++const MESSAGES = [{ role: 'user', content: 'Pick the one moment worth clipping.' }];++const empty = {++ model: 'deepseek-flash',++ choices: [{ index: 0, message: { role: 'assistant', content: '', reasoning_content: 'Still thinking about the one moment.' }, finish_reason: 'length' }],++ usage: { prompt_tokens: 9556, completion_tokens: 48, total_tokens: 9604, completion_tokens_details: { reasoning_tokens: 48 } },++};++let h;++let srv;++let route;++++t.test('the real DeepSeek shape: content is the answer, reasoning is not, output excludes reasoning tokens', async () => {++ const s = await seamServer(() => completion);++ try {++ const a = createOpenAiProvider({ key: 'deepseek', base_url: `${s.url}/v1`, capabilities: ['chat'] }, { apiKey: 'k' });++ const r = await a.chat({ model: 'deepseek-flash', messages: MESSAGES, maxTokens: 300, timeoutMs: 3000 });++ assert.strictEqual(r.text, answer);++ assert.ok(!r.text.includes('repeats the same scene'), 'reasoning_content never becomes the answer');++ assert.strictEqual(r.usage.input, 9556);++ assert.strictEqual(r.usage.output, 28, 'completion_tokens 182 minus 154 reasoning');++ assert.strictEqual(r.usage.reasoning, 154);++ } finally { await s.close(); }++});++++t.test('boot a DeepSeek Flash offer', async () => {++ srv = await seamServer((body) => (body.max_tokens >= 1000 ? completion : empty));++ h = await boot({ env: { AI_STUB_FALLBACK: 'false', AI_PROVIDER_RETRIES: '0' } });++ await h.registry.upsertProvider({++ key: 'deepseek', display_name: 'DeepSeek', kind: 'openai', status: 'active', base_url: `${srv.url}/v1`,++ auth_mode: 'none', capabilities: ['chat', 'generate', 'json'], default_model: 'deepseek-flash', timeout_ms: 3000,++ }, { actor: 'test', origin: 'admin' });++ await h.registry.upsertModel({++ provider_key: 'deepseek', model_key: 'deepseek-flash', display_name: 'DeepSeek V4.1 Flash', type: 'chat',++ supports: { json: true, tools: true, streaming: true }, cost: { in_per_mtok: 0.3, out_per_mtok: 1.2 },++ metadata: { reasoning: true, reasoning_reserve_tokens: 1536 },++ }, { actor: 'test' });++ route = await h.registry.createRouteVersion('test.deepseek-flash', { capability: 'chat', timeout_ms: 3000 }, { actor: 'test' });++});++++t.test('a 48-token caller cap gets the model\'s reasoning room and answers', async () => {++ const out = await h.pool.execute(route, 'chat', { messages: MESSAGES, maxTokens: 48, timeoutMs: 3000 },++ { routeKey: route.key, routeVersion: route.version });++ assert.strictEqual(srv.last.max_tokens, 48 + 1536, 'the caller\'s 48 plus the registry reserve');++ assert.strictEqual(out.provider, 'deepseek');++ assert.strictEqual(out.result.text, answer);++ assert.strictEqual(out.usage.output, 28);++});++++t.test('the Exchange chat path (complete) gets the same reasoning room', async () => {++ const out = await h.pool.execute(route, 'complete', { openai: { messages: MESSAGES }, maxTokens: 48, timeoutMs: 3000 },++ { routeKey: route.key, routeVersion: route.version });++ assert.strictEqual(srv.last.max_tokens, 48 + 1536);++ assert.strictEqual(out.result.text, answer);++});++++t.test('without the reserve the same 48 tokens are all reasoning and the answer is empty', async () => {++ await h.registry.upsertModel({++ provider_key: 'deepseek', model_key: 'deepseek-flash', display_name: 'DeepSeek V4.1 Flash', type: 'chat',++ supports: { json: true, tools: true, streaming: true }, cost: { in_per_mtok: 0.3, out_per_mtok: 1.2 },++ metadata: { reasoning: true },++ }, { actor: 'test' });++ const out = await h.pool.execute(route, 'chat', { messages: MESSAGES, maxTokens: 48, timeoutMs: 3000 },++ { routeKey: route.key, routeVersion: route.version });++ assert.strictEqual(srv.last.max_tokens, 48, 'no reserve means no extra room');++ assert.strictEqual(out.result.text, '', 'the reproduction of the production run');++ assert.strictEqual(out.usage.output, 0);++});++++t.test('the seed puts the reasoning room on the model, not on every provider', async () => {++ const b = await boot({ env: { AI_STUB_FALLBACK: 'false', DEEPSEEK_API_KEY: 'test-key', DEEPSEEK_KEY: 'test-key' } });++ try {++ const m = await b.registry.getModel('deepseek', 'deepseek-flash');++ assert.ok(m, 'the seed creates DeepSeek Flash');++ assert.strictEqual(m.metadata.reasoning_reserve_tokens, 1536);++ } finally { await b.stop(); }++});++++t.test('stop', async () => { await h.stop(); await srv.close(); });++++t.run();@@@ -1,7 +1,7 @@'use strict';--// The catalog's uptime is per real provider attempt, not per run status: a failed run counts against an--// offer only when an attempt row names it, and an ok attempt is a success even when the run afterwards--// failed validation (output.empty). Uptime and speed need 20 samples; below that they are null.++// The catalog's uptime is per real provider attempt, not per run status. A failed run counts against an++// offer only when an attempt row names it; an ok attempt is a success only when it produced output (not an++// output.empty run, and not a text attempt that spent every token on reasoning). 20 samples before a number.const assert = require('assert');const { boot, request, suite } = require('./helpers');@@@ -15,16 +15,17 @@ const clock = { now: () => nowMs };const at = (n) => new Date(nowMs - n * 86400000).toISOString();const uid = (p) => `${p}_${Date.now()}_${seq++}`;--async function insertRun({ id = uid('run'), status = 'succeeded', provider = null, model = null, created = at(0) } = {}) {++async function insertRun({ id = uid('run'), status = 'succeeded', provider = null, model = null, errorCode = null, created = at(0) } = {}) {await h.db.prepare(`INSERT INTO runs (id, workflow_key, workflow_version, status, requester_type, requester_id, input, input_hash,-- provider_key, model_key, created_at, started_at, finished_at)-- VALUES (?, 'ai.complete', 1, ?, 'service', 'test', '{}', 'input-hash', ?, ?, ?, ?, ?)`)-- .run(id, status, provider, model, created, created, created);++ provider_key, model_key, error_code, created_at, started_at, finished_at)++ VALUES (?, 'ai.complete', 1, ?, 'service', 'test', '{}', 'input-hash', ?, ?, ?, ?, ?, ?)`)++ .run(id, status, provider, model, errorCode, created, created, created);return id;}--async function insertRequest({ runId, provider = 'deepseek', model = 'deepseek-flash', status = 'ok', latency = 100, created = at(0), seqNo = 0 }) {-- await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, latency_ms, created_at)-- VALUES (?, ?, 'chat', ?, ?, ?, ?, ?)`).run(runId, seqNo, provider, model, status, latency, created);++async function insertRequest({ runId, provider = 'deepseek', model = 'deepseek-flash', status = 'ok', operation = 'chat',++ tokensOut = 7, latency = 100, created = at(0), seqNo = 0 }) {++ await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, tokens_out, latency_ms, created_at)++ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(runId, seqNo, operation, provider, model, status, tokensOut, latency, created);}async function reset() {await h.db.prepare('DELETE FROM requests').run();@@@ -72,7 +73,7 @@ t.test('a failed run that reached no provider counts against no offer', async ()t.test('a failed run counts against the offer its failed attempt names', async () => {await reset();const failed = await insertRun({ status: 'failed' });-- await insertRequest({ runId: failed, status: 'error', latency: 900 });++ await insertRequest({ runId: failed, status: 'error', tokensOut: 0, latency: 900 });for (let i = 0; i < 19; i++) {const id = await insertRun();await insertRequest({ runId: id });@@@ -83,23 +84,29 @@ t.test('a failed run counts against the offer its failed attempt names', async (assert.strictEqual(offer.uptime, 0.95, '19 answered attempts and 1 failed attempt');});--t.test('an ok attempt is a success even when its run failed validation', async () => {++t.test('an ok attempt that produced no output is a failure, not a success', async () => {await reset();-- // Every run failed with output.empty, but every provider attempt answered ok (production's 8 Flash runs).-- for (let i = 0; i < 20; i++) {-- const id = await insertRun({ status: 'failed' });-- await insertRequest({ runId: id });++ // 10 runs ended output.empty; their provider answered but produced nothing usable.++ for (let i = 0; i < 10; i++) {++ const id = await insertRun({ status: 'failed', errorCode: 'output.empty' });++ await insertRequest({ runId: id, tokensOut: 0 });++ }++ // 10 more spent every output token on reasoning: tokens_out 0 with no content.++ for (let i = 0; i < 10; i++) {++ const id = await insertRun();++ await insertRequest({ runId: id, operation: 'generate', tokensOut: 0 });}const offer = flash(await catalog());assert.strictEqual(offer.requests_7d, 20);-- assert.strictEqual(offer.uptime, 1, '20 answered attempts, 0 failed attempts');++ assert.strictEqual(offer.uptime, 0, '20 ok-but-empty Flash attempts are 20 failures');++ assert.deepStrictEqual(offer.speed, { p50_ms: null, p95_ms: null }, 'an empty attempt is not a speed sample');});t.test('8 successes and 0 real failures are 100% only once 20 samples exist', async () => {await reset();for (let i = 0; i < 8; i++) {-- const id = await insertRun({ status: 'failed' });++ const id = await insertRun();await insertRequest({ runId: id });}let offer = flash(await catalog());@@@ -21,9 +21,9 @@ async function insertRun({ id = uid('run'), status = 'succeeded', provider = nul.run(id, status, type, requester, provider, model, cachedFrom, tokensIn, tokensOut, cost, synthetic, created, created, created);return id;}--async function insertRequest({ runId, provider, model, status = 'ok', latency = 100, created = at(0), seqNo = 0 }) {-- await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, latency_ms, created_at)-- VALUES (?, ?, 'chat', ?, ?, ?, ?, ?)`).run(runId, seqNo, provider, model, status, latency, created);++async function insertRequest({ runId, provider, model, status = 'ok', tokensOut = 100, latency = 100, created = at(0), seqNo = 0 }) {++ await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, tokens_out, latency_ms, created_at)++ VALUES (?, ?, 'chat', ?, ?, ?, ?, ?, ?)`).run(runId, seqNo, provider, model, status, tokensOut, latency, created);}async function insertReceipt({ runId, body }) {await h.db.prepare(`INSERT INTO ai_receipts (id, run_id, body) VALUES (?, ?, ?::jsonb)`)nnew file mode 100644@@@ -0,0 +1,31 @@++{++ "_comment": "The shape DeepSeek V4.1 Flash returned on 2026-11-07 (one real call, 9.5k-token prompt, max_tokens 300): the answer is in choices[0].message.content and the thinking in reasoning_content, with reasoning_tokens counted inside completion_tokens. Identifiers are redacted.",++ "completion": {++ "id": "chatcmpl-redacted",++ "object": "chat.completion",++ "created": 0,++ "model": "deepseek-flash",++ "choices": [++ {++ "index": 0,++ "message": {++ "role": "assistant",++ "content": "{\"at\":\"0:00\",\"why\":\"All repeated descriptions are identical, so the beginning is the only identifiable moment to clip.\"}",++ "reasoning_content": "The prompt repeats the same scene; the only identifiable moment is the start. I should return strict JSON with one sentence."++ },++ "logprobs": null,++ "finish_reason": "stop"++ }++ ],++ "usage": {++ "prompt_tokens": 9556,++ "completion_tokens": 182,++ "total_tokens": 9738,++ "prompt_tokens_details": { "cached_tokens": 0 },++ "completion_tokens_details": { "reasoning_tokens": 154 },++ "prompt_cache_hit_tokens": 0,++ "prompt_cache_miss_tokens": 9556++ },++ "system_fingerprint": "redacted"++ }++}
@@ -11,9 +11,10 @@
@@ -11,9 +11,10 @@
*
*
* Uptime is per real provider attempt, not per run status. A failed run counts against an offer only
* Uptime is per real provider attempt, not per run status. A failed run counts against an offer only
* when an attempt row names that provider and model; a run refused before routing, over quota or by
* when an attempt row names that provider and model; a run refused before routing, over quota or by
- * validation has no such row and counts against nothing. An attempt that answered (status ok) is a
- * success even when the run afterwards failed validation (`output.empty` / `output.invalid`): the
- * offer did answer. A skipped or cancelled row was never an answer and is not a sample.
+ * validation has no such row and counts against nothing. An attempt is a success only when it produced
+ * output: an ok row whose run ended `output.empty`, or a text attempt that spent every output token
+ * (tokens_out = 0, e.g. all of max_tokens went to reasoning), is a failure of that provider and model.
+ * A skipped or cancelled row was never an answer and is not a sample.
*
*
* The latency of an answer is its time to the first token when the receipt recorded one (a streamed
* The latency of an answer is its time to the first token when the receipt recorded one (a streamed
* answer), else the total provider latency of the attempt that answered. Uptime and speed need enough
* answer), else the total provider latency of the attempt that answered. Uptime and speed need enough
@@ -23,6 +24,8 @@
@@ -23,6 +24,8 @@
*/
*/
const DAY_MS = 86400000;
const DAY_MS = 86400000;
const MIN_SAMPLES = 20;
const MIN_SAMPLES = 20;
+// sha256(''): what the router stores as a request's output_hash when the provider produced no content.
+const EMPTY_OUTPUT_HASH = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
const dayOf = (ms) => new Date(ms).toISOString().slice(0, 10);
const dayOf = (ms) => new Date(ms).toISOString().slice(0, 10);
// What an upstream appends to a registry name: -2025-08-07, -v3-0324, -4-5-20251001, :free, @rev.
// What an upstream appends to a registry name: -2025-08-07, -v3-0324, -4-5-20251001, :free, @rev.
@@ -60,11 +63,19 @@ function percentile(values, p) {
@@ -60,11 +63,19 @@ function percentile(values, p) {
}
}
function createObserved({ db, clock = { now: () => Date.now() } }) {
function createObserved({ db, clock = { now: () => Date.now() } }) {
- // One sample per real provider attempt: ok is a success, error or timeout is a failure. Skipped and
- // cancelled rows are not samples (no provider was tried, or the caller left). The run's own provider
- // and model are not used here: a fallback run can name several offers, and a failed run keeps none.
+ // One sample per real provider attempt: ok with output is a success; error, timeout, an empty run
+ // and a text attempt that spent every token are failures. Skipped and cancelled rows are not samples
+ // (no provider was tried, or the caller left). The run's own provider and model are not used here: a
+ // fallback run can name several offers, and a failed run keeps none.
const attemptRows = db.prepare(`SELECT to_char(r.created_at::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,
const attemptRows = db.prepare(`SELECT to_char(r.created_at::timestamptz AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day,
- CASE q.status WHEN 'ok' THEN 'succeeded' ELSE 'failed' END AS status,
+ CASE
+ WHEN q.status <> 'ok' THEN 'failed'
+ WHEN r.error_code = 'output.empty' THEN 'failed'
+ WHEN q.operation IN ('chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages')
+ AND COALESCE(q.tokens_out, 0) = 0
+ AND (q.output_hash IS NULL OR q.output_hash = '${EMPTY_OUTPUT_HASH}') THEN 'failed'
+ ELSE 'succeeded'
+ END AS status,
q.provider_key AS provider_key, q.model_key AS model_key,
q.provider_key AS provider_key, q.model_key AS model_key,
COUNT(*)::bigint AS n
COUNT(*)::bigint AS n
FROM requests q
FROM requests q
@@ -92,6 +103,10 @@ function createObserved({ db, clock = { now: () => Date.now() } }) {
@@ -92,6 +103,10 @@ function createObserved({ db, clock = { now: () => Date.now() } }) {
LEFT JOIN ai_receipts rc ON rc.run_id = r.id
LEFT JOIN ai_receipts rc ON rc.run_id = r.id
WHERE q.status = 'ok' AND q.latency_ms IS NOT NULL
WHERE q.status = 'ok' AND q.latency_ms IS NOT NULL
AND r.status IN ('succeeded', 'failed')
AND r.status IN ('succeeded', 'failed')
+ AND (r.error_code IS NULL OR r.error_code <> 'output.empty')
+ AND NOT (q.operation IN ('chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages')
+ AND COALESCE(q.tokens_out, 0) = 0
+ AND (q.output_hash IS NULL OR q.output_hash = '${EMPTY_OUTPUT_HASH}'))
AND r.created_at::timestamptz >= ?::timestamptz AND r.created_at::timestamptz <= ?::timestamptz`);
AND r.created_at::timestamptz >= ?::timestamptz AND r.created_at::timestamptz <= ?::timestamptz`);
/** The real attempts and cached answers of the window, and the UTC day that is "today". */
/** The real attempts and cached answers of the window, and the UTC day that is "today". */
@@ -1385,7 +1385,16 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
@@ -1385,7 +1385,16 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
if (uncensored) { attemptFacts.content_policy = 'uncensored'; attemptFacts.label = tier.LABEL; }
if (uncensored) { attemptFacts.content_policy = 'uncensored'; attemptFacts.label = tier.LABEL; }
// The hold needs the offer's retail card, whether it is prepaid, and the real output cap.
// The hold needs the offer's retail card, whether it is prepaid, and the real output cap.
const attemptRow = p && model ? await registry.getModel(p.key, model) : null;
const attemptRow = p && model ? await registry.getModel(p.key, model) : null;
- const attemptMeta = { provider: p ? p.key : null, model, maxTokens: req.maxTokens, paid: !isFreeProvider(p), retail: modelCatalog.retailRate(attemptRow) };
+ // A reasoning model spends the same max_tokens allowance thinking before it writes: a caller's
+ // 300-token cap can be all reasoning, leaving content empty (finish_reason 'length', output 0).
+ // The registry says how much room this model needs on top of the answer the caller asked for.
+ const reserve = Number(attemptRow && attemptRow.metadata && attemptRow.metadata.reasoning_reserve_tokens) || 0;
+ const requestedMax = Number(req.maxTokens);
+ const modelMax = reserve > 0 && Number.isSafeInteger(requestedMax) && requestedMax >= 1
+ && ['chat', 'generate', 'summarize', 'classify', 'extract', 'enrich', 'complete', 'messages'].includes(operation)
+ ? requestedMax + reserve
+ : req.maxTokens;
+ const attemptMeta = { provider: p ? p.key : null, model, maxTokens: modelMax, paid: !isFreeProvider(p), retail: modelCatalog.retailRate(attemptRow) };
const routed = typeof ctx.onRoute === 'function' ? await ctx.onRoute(attemptFacts, attemptMeta) : null;
const routed = typeof ctx.onRoute === 'function' ? await ctx.onRoute(attemptFacts, attemptMeta) : null;
if (routed && routed.skip) {
if (routed && routed.skip) {
statFor(c.provider).skipped++;
statFor(c.provider).skipped++;
@@ -1395,8 +1404,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
@@ -1395,8 +1404,9 @@ function createProviderPool({ db, registry, config, clock = { now: () => Date.no
continue candidate;
continue candidate;
}
}
// max_cost_usd, when it binds, comes back as the affordable output cap for this attempt.
// max_cost_usd, when it binds, comes back as the affordable output cap for this attempt.
- const callMax = routed && Number.isSafeInteger(routed.maxTokens) && routed.maxTokens >= 1 && routed.maxTokens !== req.maxTokens ? routed.maxTokens : null;
- let callReq = callMax == null ? req : { ...req, maxTokens: callMax };
+ const callMax = routed && Number.isSafeInteger(routed.maxTokens) && routed.maxTokens >= 1 && routed.maxTokens !== modelMax ? routed.maxTokens : null;
+ let callReq = modelMax !== req.maxTokens ? { ...req, maxTokens: modelMax } : req;
+ if (callMax != null) callReq = { ...callReq, maxTokens: callMax };
// The host's model writes a short answer: its own cap, never more, whatever the route asked for.
// The host's model writes a short answer: its own cap, never more, whatever the route asked for.
if (local && operation !== 'embed') {
if (local && operation !== 'embed') {
const localMax = Math.max(1, (config.localLlm && config.localLlm.maxTokens) || 512);
const localMax = Math.max(1, (config.localLlm && config.localLlm.maxTokens) || 512);
@@ -35,14 +35,14 @@ const DEEPSEEK_MODELS = [
@@ -35,14 +35,14 @@ const DEEPSEEK_MODELS = [
context_window: 1048576, max_output: 393216,
context_window: 1048576, max_output: 393216,
supports: { json: true, tools: true, streaming: true, vision: true },
supports: { json: true, tools: true, streaming: true, vision: true },
cost: { in_per_mtok: 0.30, out_per_mtok: 1.20, cached_per_mtok: 0.006, source: 'https://api-docs.deepseek.com/quick_start/pricing', verified_at: '2026-11-06' },
cost: { in_per_mtok: 0.30, out_per_mtok: 1.20, cached_per_mtok: 0.006, source: 'https://api-docs.deepseek.com/quick_start/pricing', verified_at: '2026-11-06' },
- metadata: { org: 'deepseek', model: 'deepseek-flash', family: 'DeepSeek', tier: 'fast', reasoning: true, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.15 in, $0.003 cached, $0.60 out per 1M.' },
+ metadata: { org: 'deepseek', model: 'deepseek-flash', family: 'DeepSeek', tier: 'fast', reasoning: true, reasoning_reserve_tokens: 1536, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.15 in, $0.003 cached, $0.60 out per 1M.' },
},
},
{
{
model_key: 'deepseek-v4-pro', display_name: 'DeepSeek V4 Pro', type: 'chat',
model_key: 'deepseek-v4-pro', display_name: 'DeepSeek V4 Pro', type: 'chat',
context_window: 1048576, max_output: 393216,
context_window: 1048576, max_output: 393216,
supports: { json: true, tools: true, streaming: true, vision: false },
supports: { json: true, tools: true, streaming: true, vision: false },
cost: { in_per_mtok: 1.32, out_per_mtok: 3.96, cached_per_mtok: 0.044, source: 'https://api-docs.deepseek.com/quick_start/pricing', verified_at: '2026-11-06' },
cost: { in_per_mtok: 1.32, out_per_mtok: 3.96, cached_per_mtok: 0.044, source: 'https://api-docs.deepseek.com/quick_start/pricing', verified_at: '2026-11-06' },
- metadata: { org: 'deepseek', model: 'deepseek-v4-pro', family: 'DeepSeek', tier: 'strong', reasoning: true, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.66 in, $0.022 cached, $1.98 out per 1M.' },
+ metadata: { org: 'deepseek', model: 'deepseek-v4-pro', family: 'DeepSeek', tier: 'strong', reasoning: true, reasoning_reserve_tokens: 1536, configured_by: 'DEEPSEEK_API_KEY', pricing_note: 'Peak rates (01:00-04:00 and 06:00-10:00 UTC, Mon-Fri); off-peak is half: $0.66 in, $0.022 cached, $1.98 out per 1M.' },
},
},
];
];
new file mode 100644
new file mode 100644
@@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
+'use strict';
+// DeepSeek V4.1 Flash is a reasoning model: `max_tokens` is the whole allowance, and reasoning is spent
+// from it before content. A 48-token cap can therefore be all thinking, finish_reason 'length', content
+// empty and usage output 0. The real response (2026-11-07) keeps the answer in choices[0].message.content
+// and the thinking in reasoning_content; usage counts reasoning_tokens inside completion_tokens. The
+// adapter must read content, and the registry says how much room this model needs beyond the caller's cap.
+const assert = require('assert');
+const fixture = require('./fixtures/deepseek-flash.json');
+const { boot, suite, seamServer } = require('./helpers');
+const { createOpenAiProvider } = require('../server/providers/openai');
+
+const t = suite('deepseek-flash');
+const completion = fixture.completion;
+const answer = completion.choices[0].message.content;
+const MESSAGES = [{ role: 'user', content: 'Pick the one moment worth clipping.' }];
+const empty = {
+ model: 'deepseek-flash',
+ choices: [{ index: 0, message: { role: 'assistant', content: '', reasoning_content: 'Still thinking about the one moment.' }, finish_reason: 'length' }],
+ usage: { prompt_tokens: 9556, completion_tokens: 48, total_tokens: 9604, completion_tokens_details: { reasoning_tokens: 48 } },
+};
+let h;
+let srv;
+let route;
+
+t.test('the real DeepSeek shape: content is the answer, reasoning is not, output excludes reasoning tokens', async () => {
+ const s = await seamServer(() => completion);
+ try {
+ const a = createOpenAiProvider({ key: 'deepseek', base_url: `${s.url}/v1`, capabilities: ['chat'] }, { apiKey: 'k' });
+ const r = await a.chat({ model: 'deepseek-flash', messages: MESSAGES, maxTokens: 300, timeoutMs: 3000 });
+ assert.strictEqual(r.text, answer);
+ assert.ok(!r.text.includes('repeats the same scene'), 'reasoning_content never becomes the answer');
+ assert.strictEqual(r.usage.input, 9556);
+ assert.strictEqual(r.usage.output, 28, 'completion_tokens 182 minus 154 reasoning');
+ assert.strictEqual(r.usage.reasoning, 154);
+ } finally { await s.close(); }
+});
+
+t.test('boot a DeepSeek Flash offer', async () => {
+ srv = await seamServer((body) => (body.max_tokens >= 1000 ? completion : empty));
+ h = await boot({ env: { AI_STUB_FALLBACK: 'false', AI_PROVIDER_RETRIES: '0' } });
+ await h.registry.upsertProvider({
+ key: 'deepseek', display_name: 'DeepSeek', kind: 'openai', status: 'active', base_url: `${srv.url}/v1`,
+ auth_mode: 'none', capabilities: ['chat', 'generate', 'json'], default_model: 'deepseek-flash', timeout_ms: 3000,
+ }, { actor: 'test', origin: 'admin' });
+ await h.registry.upsertModel({
+ provider_key: 'deepseek', model_key: 'deepseek-flash', display_name: 'DeepSeek V4.1 Flash', type: 'chat',
+ supports: { json: true, tools: true, streaming: true }, cost: { in_per_mtok: 0.3, out_per_mtok: 1.2 },
+ metadata: { reasoning: true, reasoning_reserve_tokens: 1536 },
+ }, { actor: 'test' });
+ route = await h.registry.createRouteVersion('test.deepseek-flash', { capability: 'chat', timeout_ms: 3000 }, { actor: 'test' });
+});
+
+t.test('a 48-token caller cap gets the model\'s reasoning room and answers', async () => {
+ const out = await h.pool.execute(route, 'chat', { messages: MESSAGES, maxTokens: 48, timeoutMs: 3000 },
+ { routeKey: route.key, routeVersion: route.version });
+ assert.strictEqual(srv.last.max_tokens, 48 + 1536, 'the caller\'s 48 plus the registry reserve');
+ assert.strictEqual(out.provider, 'deepseek');
+ assert.strictEqual(out.result.text, answer);
+ assert.strictEqual(out.usage.output, 28);
+});
+
+t.test('the Exchange chat path (complete) gets the same reasoning room', async () => {
+ const out = await h.pool.execute(route, 'complete', { openai: { messages: MESSAGES }, maxTokens: 48, timeoutMs: 3000 },
+ { routeKey: route.key, routeVersion: route.version });
+ assert.strictEqual(srv.last.max_tokens, 48 + 1536);
+ assert.strictEqual(out.result.text, answer);
+});
+
+t.test('without the reserve the same 48 tokens are all reasoning and the answer is empty', async () => {
+ await h.registry.upsertModel({
+ provider_key: 'deepseek', model_key: 'deepseek-flash', display_name: 'DeepSeek V4.1 Flash', type: 'chat',
+ supports: { json: true, tools: true, streaming: true }, cost: { in_per_mtok: 0.3, out_per_mtok: 1.2 },
+ metadata: { reasoning: true },
+ }, { actor: 'test' });
+ const out = await h.pool.execute(route, 'chat', { messages: MESSAGES, maxTokens: 48, timeoutMs: 3000 },
+ { routeKey: route.key, routeVersion: route.version });
+ assert.strictEqual(srv.last.max_tokens, 48, 'no reserve means no extra room');
+ assert.strictEqual(out.result.text, '', 'the reproduction of the production run');
+ assert.strictEqual(out.usage.output, 0);
+});
+
+t.test('the seed puts the reasoning room on the model, not on every provider', async () => {
+ const b = await boot({ env: { AI_STUB_FALLBACK: 'false', DEEPSEEK_API_KEY: 'test-key', DEEPSEEK_KEY: 'test-key' } });
+ try {
+ const m = await b.registry.getModel('deepseek', 'deepseek-flash');
+ assert.ok(m, 'the seed creates DeepSeek Flash');
+ assert.strictEqual(m.metadata.reasoning_reserve_tokens, 1536);
+ } finally { await b.stop(); }
+});
+
+t.test('stop', async () => { await h.stop(); await srv.close(); });
+
+t.run();
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
-// The catalog's uptime is per real provider attempt, not per run status: a failed run counts against an
-// offer only when an attempt row names it, and an ok attempt is a success even when the run afterwards
-// failed validation (output.empty). Uptime and speed need 20 samples; below that they are null.
+// The catalog's uptime is per real provider attempt, not per run status. A failed run counts against an
+// offer only when an attempt row names it; an ok attempt is a success only when it produced output (not an
+// output.empty run, and not a text attempt that spent every token on reasoning). 20 samples before a number.
const assert = require('assert');
const assert = require('assert');
const { boot, request, suite } = require('./helpers');
const { boot, request, suite } = require('./helpers');
@@ -15,16 +15,17 @@ const clock = { now: () => nowMs };
@@ -15,16 +15,17 @@ const clock = { now: () => nowMs };
const at = (n) => new Date(nowMs - n * 86400000).toISOString();
const at = (n) => new Date(nowMs - n * 86400000).toISOString();
const uid = (p) => `${p}_${Date.now()}_${seq++}`;
const uid = (p) => `${p}_${Date.now()}_${seq++}`;
-async function insertRun({ id = uid('run'), status = 'succeeded', provider = null, model = null, created = at(0) } = {}) {
+async function insertRun({ id = uid('run'), status = 'succeeded', provider = null, model = null, errorCode = null, created = at(0) } = {}) {
await h.db.prepare(`INSERT INTO runs (id, workflow_key, workflow_version, status, requester_type, requester_id, input, input_hash,
await h.db.prepare(`INSERT INTO runs (id, workflow_key, workflow_version, status, requester_type, requester_id, input, input_hash,
- provider_key, model_key, created_at, started_at, finished_at)
- VALUES (?, 'ai.complete', 1, ?, 'service', 'test', '{}', 'input-hash', ?, ?, ?, ?, ?)`)
- .run(id, status, provider, model, created, created, created);
+ provider_key, model_key, error_code, created_at, started_at, finished_at)
+ VALUES (?, 'ai.complete', 1, ?, 'service', 'test', '{}', 'input-hash', ?, ?, ?, ?, ?, ?)`)
+ .run(id, status, provider, model, errorCode, created, created, created);
return id;
return id;
}
}
-async function insertRequest({ runId, provider = 'deepseek', model = 'deepseek-flash', status = 'ok', latency = 100, created = at(0), seqNo = 0 }) {
- await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, latency_ms, created_at)
- VALUES (?, ?, 'chat', ?, ?, ?, ?, ?)`).run(runId, seqNo, provider, model, status, latency, created);
+async function insertRequest({ runId, provider = 'deepseek', model = 'deepseek-flash', status = 'ok', operation = 'chat',
+ tokensOut = 7, latency = 100, created = at(0), seqNo = 0 }) {
+ await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, tokens_out, latency_ms, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(runId, seqNo, operation, provider, model, status, tokensOut, latency, created);
}
}
async function reset() {
async function reset() {
await h.db.prepare('DELETE FROM requests').run();
await h.db.prepare('DELETE FROM requests').run();
@@ -72,7 +73,7 @@ t.test('a failed run that reached no provider counts against no offer', async ()
@@ -72,7 +73,7 @@ t.test('a failed run that reached no provider counts against no offer', async ()
t.test('a failed run counts against the offer its failed attempt names', async () => {
t.test('a failed run counts against the offer its failed attempt names', async () => {
await reset();
await reset();
const failed = await insertRun({ status: 'failed' });
const failed = await insertRun({ status: 'failed' });
- await insertRequest({ runId: failed, status: 'error', latency: 900 });
+ await insertRequest({ runId: failed, status: 'error', tokensOut: 0, latency: 900 });
for (let i = 0; i < 19; i++) {
for (let i = 0; i < 19; i++) {
const id = await insertRun();
const id = await insertRun();
await insertRequest({ runId: id });
await insertRequest({ runId: id });
@@ -83,23 +84,29 @@ t.test('a failed run counts against the offer its failed attempt names', async (
@@ -83,23 +84,29 @@ t.test('a failed run counts against the offer its failed attempt names', async (
assert.strictEqual(offer.uptime, 0.95, '19 answered attempts and 1 failed attempt');
assert.strictEqual(offer.uptime, 0.95, '19 answered attempts and 1 failed attempt');
});
});
-t.test('an ok attempt is a success even when its run failed validation', async () => {
+t.test('an ok attempt that produced no output is a failure, not a success', async () => {
await reset();
await reset();
- // Every run failed with output.empty, but every provider attempt answered ok (production's 8 Flash runs).
- for (let i = 0; i < 20; i++) {
- const id = await insertRun({ status: 'failed' });
- await insertRequest({ runId: id });
+ // 10 runs ended output.empty; their provider answered but produced nothing usable.
+ for (let i = 0; i < 10; i++) {
+ const id = await insertRun({ status: 'failed', errorCode: 'output.empty' });
+ await insertRequest({ runId: id, tokensOut: 0 });
+ }
+ // 10 more spent every output token on reasoning: tokens_out 0 with no content.
+ for (let i = 0; i < 10; i++) {
+ const id = await insertRun();
+ await insertRequest({ runId: id, operation: 'generate', tokensOut: 0 });
}
}
const offer = flash(await catalog());
const offer = flash(await catalog());
assert.strictEqual(offer.requests_7d, 20);
assert.strictEqual(offer.requests_7d, 20);
- assert.strictEqual(offer.uptime, 1, '20 answered attempts, 0 failed attempts');
+ assert.strictEqual(offer.uptime, 0, '20 ok-but-empty Flash attempts are 20 failures');
+ assert.deepStrictEqual(offer.speed, { p50_ms: null, p95_ms: null }, 'an empty attempt is not a speed sample');
});
});
t.test('8 successes and 0 real failures are 100% only once 20 samples exist', async () => {
t.test('8 successes and 0 real failures are 100% only once 20 samples exist', async () => {
await reset();
await reset();
for (let i = 0; i < 8; i++) {
for (let i = 0; i < 8; i++) {
- const id = await insertRun({ status: 'failed' });
+ const id = await insertRun();
await insertRequest({ runId: id });
await insertRequest({ runId: id });
}
}
let offer = flash(await catalog());
let offer = flash(await catalog());
@@ -21,9 +21,9 @@ async function insertRun({ id = uid('run'), status = 'succeeded', provider = nul
@@ -21,9 +21,9 @@ async function insertRun({ id = uid('run'), status = 'succeeded', provider = nul
.run(id, status, type, requester, provider, model, cachedFrom, tokensIn, tokensOut, cost, synthetic, created, created, created);
.run(id, status, type, requester, provider, model, cachedFrom, tokensIn, tokensOut, cost, synthetic, created, created, created);
return id;
return id;
}
}
-async function insertRequest({ runId, provider, model, status = 'ok', latency = 100, created = at(0), seqNo = 0 }) {
- await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, latency_ms, created_at)
- VALUES (?, ?, 'chat', ?, ?, ?, ?, ?)`).run(runId, seqNo, provider, model, status, latency, created);
+async function insertRequest({ runId, provider, model, status = 'ok', tokensOut = 100, latency = 100, created = at(0), seqNo = 0 }) {
+ await h.db.prepare(`INSERT INTO requests (run_id, seq, operation, provider_key, model_key, status, tokens_out, latency_ms, created_at)
+ VALUES (?, ?, 'chat', ?, ?, ?, ?, ?, ?)`).run(runId, seqNo, provider, model, status, tokensOut, latency, created);
}
}
async function insertReceipt({ runId, body }) {
async function insertReceipt({ runId, body }) {
await h.db.prepare(`INSERT INTO ai_receipts (id, run_id, body) VALUES (?, ?, ?::jsonb)`)
await h.db.prepare(`INSERT INTO ai_receipts (id, run_id, body) VALUES (?, ?, ?::jsonb)`)
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+{
+ "_comment": "The shape DeepSeek V4.1 Flash returned on 2026-11-07 (one real call, 9.5k-token prompt, max_tokens 300): the answer is in choices[0].message.content and the thinking in reasoning_content, with reasoning_tokens counted inside completion_tokens. Identifiers are redacted.",
+ "completion": {
+ "id": "chatcmpl-redacted",
+ "object": "chat.completion",
+ "created": 0,
+ "model": "deepseek-flash",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "{\"at\":\"0:00\",\"why\":\"All repeated descriptions are identical, so the beginning is the only identifiable moment to clip.\"}",
+ "reasoning_content": "The prompt repeats the same scene; the only identifiable moment is the start. I should return strict JSON with one sentence."
+ },
+ "logprobs": null,
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 9556,
+ "completion_tokens": 182,
+ "total_tokens": 9738,
+ "prompt_tokens_details": { "cached_tokens": 0 },
+ "completion_tokens_details": { "reasoning_tokens": 154 },
+ "prompt_cache_hit_tokens": 0,
+ "prompt_cache_miss_tokens": 9556
+ },
+ "system_fingerprint": "redacted"
+ }
+}