fix: retain every direct-model usage call
Multiple model invocations in one task could share a call identifier when a provider omitted or reused its response ID. Database upserts then replaced earlier tool-round or summary usage, causing cumulative activity totals to undercount successful calls. Each completed direct-model invocation now receives a unique local call ID while retaining the provider ID as diagnostic context. Tool rounds, final responses, image calls, and repeated compression summaries are therefore stored independently. Release note: 修复连续工具调用或上下文摘要可能覆盖前序模型用量的问题;现在每次成功调用都会分别计入运行记录和累计统计。
This commit is contained in:
@@ -363,7 +363,9 @@ describe('ModelAgentRuntime', () => {
|
|||||||
{
|
{
|
||||||
requestId: 'a431666e-5ec8-45e6-beb4-654132eed125',
|
requestId: 'a431666e-5ec8-45e6-beb4-654132eed125',
|
||||||
type: 'model-usage',
|
type: 'model-usage',
|
||||||
callId: 'message-1',
|
callId: expect.stringMatching(
|
||||||
|
/^model-call:[0-9a-f]{32}:message-1$/u
|
||||||
|
),
|
||||||
runtime: 'model',
|
runtime: 'model',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
model: 'claude-sonnet-provider',
|
model: 'claude-sonnet-provider',
|
||||||
@@ -492,7 +494,9 @@ describe('ModelAgentRuntime', () => {
|
|||||||
expect(events).toContainEqual(
|
expect(events).toContainEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: 'model-usage',
|
type: 'model-usage',
|
||||||
callId: 'context-summary:message-1'
|
callId: expect.stringMatching(
|
||||||
|
/^context-summary:model-call:[0-9a-f]{32}:message-1$/u
|
||||||
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
expect(events).toContainEqual(
|
expect(events).toContainEqual(
|
||||||
@@ -1416,7 +1420,9 @@ describe('ModelAgentRuntime', () => {
|
|||||||
{
|
{
|
||||||
requestId: 'a431666e-5ec8-45e6-beb4-654132eed127',
|
requestId: 'a431666e-5ec8-45e6-beb4-654132eed127',
|
||||||
type: 'model-usage',
|
type: 'model-usage',
|
||||||
callId: 'chatcmpl-provider-1',
|
callId: expect.stringMatching(
|
||||||
|
/^model-call:[0-9a-f]{32}:chatcmpl-provider-1$/u
|
||||||
|
),
|
||||||
runtime: 'model',
|
runtime: 'model',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
model: 'qwen3-provider',
|
model: 'qwen3-provider',
|
||||||
@@ -1613,7 +1619,9 @@ describe('ModelAgentRuntime', () => {
|
|||||||
{
|
{
|
||||||
requestId: 'a431666e-5ec8-45e6-beb4-654132eed133',
|
requestId: 'a431666e-5ec8-45e6-beb4-654132eed133',
|
||||||
type: 'model-usage',
|
type: 'model-usage',
|
||||||
callId: 'resp-provider-1',
|
callId: expect.stringMatching(
|
||||||
|
/^model-call:[0-9a-f]{32}:resp-provider-1$/u
|
||||||
|
),
|
||||||
runtime: 'model',
|
runtime: 'model',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
model: 'gpt-5-provider',
|
model: 'gpt-5-provider',
|
||||||
@@ -1660,7 +1668,6 @@ describe('ModelAgentRuntime', () => {
|
|||||||
it('runs approved direct-model tools and returns their results to OpenAI', async () => {
|
it('runs approved direct-model tools and returns their results to OpenAI', async () => {
|
||||||
const responses = [
|
const responses = [
|
||||||
{
|
{
|
||||||
id: 'chatcmpl-tool-1',
|
|
||||||
model: 'qwen3',
|
model: 'qwen3',
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
@@ -1683,7 +1690,6 @@ describe('ModelAgentRuntime', () => {
|
|||||||
usage: { prompt_tokens: 10, completion_tokens: 4 }
|
usage: { prompt_tokens: 10, completion_tokens: 4 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chatcmpl-tool-2',
|
|
||||||
model: 'qwen3',
|
model: 'qwen3',
|
||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
@@ -1817,6 +1823,27 @@ describe('ModelAgentRuntime', () => {
|
|||||||
delta: '文件内容已读取。'
|
delta: '文件内容已读取。'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
const usageEvents = events.filter(
|
||||||
|
(event) => event.type === 'model-usage'
|
||||||
|
)
|
||||||
|
expect(usageEvents).toHaveLength(2)
|
||||||
|
expect(
|
||||||
|
usageEvents.map((event) => ({
|
||||||
|
inputTokens: event.inputTokens,
|
||||||
|
outputTokens: event.outputTokens
|
||||||
|
}))
|
||||||
|
).toEqual([
|
||||||
|
{ inputTokens: 10, outputTokens: 4 },
|
||||||
|
{ inputTokens: 18, outputTokens: 7 }
|
||||||
|
])
|
||||||
|
expect(
|
||||||
|
usageEvents.every((event) =>
|
||||||
|
/^model-call:[0-9a-f]{32}$/u.test(event.callId)
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
expect(new Set(usageEvents.map((event) => event.callId)).size).toBe(
|
||||||
|
2
|
||||||
|
)
|
||||||
expect(events.at(-1)).toMatchObject({ type: 'done' })
|
expect(events.at(-1)).toMatchObject({ type: 'done' })
|
||||||
await runtime.dispose()
|
await runtime.dispose()
|
||||||
expect(toolProvider.dispose).toHaveBeenCalledOnce()
|
expect(toolProvider.dispose).toHaveBeenCalledOnce()
|
||||||
@@ -2017,6 +2044,25 @@ describe('ModelAgentRuntime', () => {
|
|||||||
expect(
|
expect(
|
||||||
contextMetrics.every((event) => event.source === 'provider')
|
contextMetrics.every((event) => event.source === 'provider')
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
|
const agentSummaryUsageEvents = events.flatMap((event) =>
|
||||||
|
event.type === 'model-usage' &&
|
||||||
|
event.callId.startsWith('agent-context-summary:')
|
||||||
|
? [event]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
expect(agentSummaryUsageEvents).toHaveLength(2)
|
||||||
|
expect(
|
||||||
|
agentSummaryUsageEvents.every((event) =>
|
||||||
|
/^agent-context-summary:model-call:[0-9a-f]{32}:message-1$/u.test(
|
||||||
|
event.callId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
new Set(
|
||||||
|
agentSummaryUsageEvents.map((event) => event.callId)
|
||||||
|
).size
|
||||||
|
).toBe(2)
|
||||||
expect(events.at(-1)).toMatchObject({ type: 'done' })
|
expect(events.at(-1)).toMatchObject({ type: 'done' })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -4271,7 +4317,9 @@ describe('ModelAgentRuntime', () => {
|
|||||||
{
|
{
|
||||||
requestId: 'a431666e-5ec8-45e6-beb4-654132eed128',
|
requestId: 'a431666e-5ec8-45e6-beb4-654132eed128',
|
||||||
type: 'model-usage',
|
type: 'model-usage',
|
||||||
callId: 'image-provider-1',
|
callId: expect.stringMatching(
|
||||||
|
/^model-call:[0-9a-f]{32}:image-provider-1$/u
|
||||||
|
),
|
||||||
runtime: 'model',
|
runtime: 'model',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
model: 'gpt-image-provider',
|
model: 'gpt-image-provider',
|
||||||
|
|||||||
@@ -483,10 +483,13 @@ function createUsageEvent(
|
|||||||
if (!usage.reported) {
|
if (!usage.reported) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
const localCallId = `model-call:${randomBytes(16).toString('hex')}`
|
||||||
return {
|
return {
|
||||||
requestId,
|
requestId,
|
||||||
type: 'model-usage',
|
type: 'model-usage',
|
||||||
callId: (usage.callId ?? requestId).slice(0, 256),
|
callId: usage.callId
|
||||||
|
? `${localCallId}:${usage.callId}`.slice(0, 256)
|
||||||
|
: localCallId,
|
||||||
runtime: 'model',
|
runtime: 'model',
|
||||||
provider,
|
provider,
|
||||||
model: (usage.model ?? fallbackModel).slice(0, 500),
|
model: (usage.model ?? fallbackModel).slice(0, 500),
|
||||||
|
|||||||
Reference in New Issue
Block a user