You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/09/07 01:26:58 UTC
(skywalking-ai-sessionizer) branch main updated: Keep the provider model on the landed record
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-ai-sessionizer.git
The following commit(s) were added to refs/heads/main by this push:
new 08e0a87 Keep the provider model on the landed record
08e0a87 is described below
commit 08e0a87f21013c8f465389a87d44a20468bfb139
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Mon Sep 7 09:26:51 2026 +0800
Keep the provider model on the landed record
A token count is reported under the model that produced it, and the
adapter dropped the model name with the envelope, so nothing landed could
say which model a call ran on. The record now carries it as model, a role
named field, and the glossary says where the runtime writes it:
message.model on an assistant record, <synthetic> on a record the client
fabricated. Both scenario writers write the same name, the cross-format
check compares it, and the well-formedness check knows the field. Records
landed before this carry none. This is the first step of the metrics work:
a metric per model can now be produced from the landed data alone.
---
docs/en/adapters/claude-code-glossary.md | 1 +
docs/en/changes/changes-0.3.0.md | 7 +++++++
docs/en/formats/asz-view-example.yaml | 18 +++++++++---------
docs/en/formats/session-data.md | 1 +
internal/adapters/claudecode/convert.go | 1 +
internal/adapters/claudecode/glossary.go | 2 ++
internal/scenario/expect/expect.go | 6 +++---
internal/scenario/write_claudecode.go | 6 +++++-
internal/scenario/write_sd.go | 4 ++--
pkg/model/term.go | 3 ++-
pkg/sessiondata/fields.go | 1 +
pkg/sessiondata/sessiondata.go | 6 ++++++
12 files changed, 40 insertions(+), 16 deletions(-)
diff --git a/docs/en/adapters/claude-code-glossary.md b/docs/en/adapters/claude-code-glossary.md
index ce4dc66..8904aaa 100644
--- a/docs/en/adapters/claude-code-glossary.md
+++ b/docs/en/adapters/claude-code-glossary.md
@@ -51,6 +51,7 @@ extraction together, because they sit in the same file.
| `message.assistant` | `text block` | an assistant record | |
| `message.external` | `origin.kind == "human"` | a user record | also a queued_command attachment whose commandMode is "prompt" |
| `message.synthetic` | `message.model == "<synthetic>"` | an assistant record | the client wrote it; no model produced it |
+| `model` | `message.model` | an assistant record | <synthetic> on a record the client fabricated |
| `notification` | `origin.kind == "task-notification"` | record | |
| `parent` | `parentUuid` | record | |
| `redacted` | `redacted_thinking` | a content block | |
diff --git a/docs/en/changes/changes-0.3.0.md b/docs/en/changes/changes-0.3.0.md
index d9cf0bc..501910a 100644
--- a/docs/en/changes/changes-0.3.0.md
+++ b/docs/en/changes/changes-0.3.0.md
@@ -2,6 +2,13 @@
> In development, not yet released. `tools/release.sh prepare 0.3.0` removes this note.
+## Collection
+
+- A landed record keeps the provider model a call ran on, as `model`, the name the runtime wrote
+ on the record. It is what a token count is reported under, so a metric per model can be
+ produced from the landed data alone. Records landed before this carry none, and the glossary
+ says where the runtime writes it.
+
## Read
- The conversation page is drawn by Horizon's conversation renderer,
`@skywalking-horizon-ui/conversation-view`, embedded from a pinned Horizon commit with Horizon's
diff --git a/docs/en/formats/asz-view-example.yaml b/docs/en/formats/asz-view-example.yaml
index 05c92a1..229a8d7 100644
--- a/docs/en/formats/asz-view-example.yaml
+++ b/docs/en/formats/asz-view-example.yaml
@@ -5,7 +5,7 @@ sessions:
- 00000001-0000-4000-8000-000000000001
head:
round: 1
- digest: 3ad0dcd4cd53fa06c502a2649b8788bfbe0a382bbc72d7f1f0fe68d9a18e96e2
+ digest: 6397f2513ecc714f1269a3609f5b1810477a4d7c702b89ec04ca674b83239595
parser: v1
policy: v1+idle=10m0s
summary:
@@ -52,11 +52,11 @@ summary:
strong_inference: 1
rounds:
- round: 1
- digest: 3ad0dcd4cd53fa06c502a2649b8788bfbe0a382bbc72d7f1f0fe68d9a18e96e2
+ digest: 6397f2513ecc714f1269a3609f5b1810477a4d7c702b89ec04ca674b83239595
previous: null
from_seq: 1
through_seq: 3
- input_digest: 6872d48ef5d3e736d0bd9f5bc03844653fb304b4b98b9ef6a27342478d950c1b
+ input_digest: 9ef1512052754d1c4fa9e9d3191e4ae15196f47bf3cdd29fd3b16feaf925c3b6
from_time: 1767225600000
through_time: 1767225611100
verified: true
@@ -69,8 +69,8 @@ files:
stream: main
run: null
lines: 18
- bytes: 5868
- digest: 944ed3d1209c76afb98f0622eb9239f95adb79fd0d0262d634c2f20bc041b1f7
+ bytes: 6044
+ digest: 7c410fe772d3c910a181e8b4228c2eca1e67cae0789c516d86033e758d9d7c32
from_time: 1767225600000
through_time: 1767225611100
- file: 00000001-0000-4000-8000-000000000001/streams/a0a10ef0666c4dc7e/transcript-20260101T000000.000000000Z-000002.sd
@@ -81,8 +81,8 @@ files:
stream: a0a10ef0666c4dc7e
run: null
lines: 4
- bytes: 995
- digest: 39b8a446c3a87d2b8da5210bba7f66ee2fd04caccea1fa0a4efdc3345b7ff7ca
+ bytes: 1019
+ digest: abe45a4107d9399bae53e17c8ba003df4f543da8b589d49623e7d9aed96314f2
from_time: 1767225606100
through_time: 1767225607100
- file: 00000001-0000-4000-8000-000000000001/streams/a0a10ef0666c4dc7e/meta-20260101T000000.000000000Z-000003.sd
@@ -97,7 +97,7 @@ files:
digest: de085eda31cdf99231fb212cb8fefbbcf95bd4f88c007c70446ecc652db3b005
from_time: null
through_time: null
- - file: _conversations/00000001-0000-4000-8000-000000000001/rounds/r000001-3ad0dcd4cd53.sf
+ - file: _conversations/00000001-0000-4000-8000-000000000001/rounds/r000001-6397f2513ecc.sf
format: sf
kind: round
seq: null
@@ -106,7 +106,7 @@ files:
run: null
lines: 45
bytes: 10468
- digest: 4f33cf8e85afcf104c823ea12be797059b4d3d10abbbb2ceb5916c2bd7e81d73
+ digest: 0004ecef47e52134e169d8a796c522776fbb317779f0ba2fe46b11b8aac4edd7
from_time: 1767225600000
through_time: 1767225611100
streams:
diff --git a/docs/en/formats/session-data.md b/docs/en/formats/session-data.md
index 2c44c77..2cde4fa 100644
--- a/docs/en/formats/session-data.md
+++ b/docs/en/formats/session-data.md
@@ -76,6 +76,7 @@ role names. The part level carries what the content is.
| `from` | who produced it: `agent`, `external`, `runtime`. A record's type is not what a record is; most records that look like a person are a tool answering. |
| `time`, `trigger`, `flags` | when, what started the loop, and states such as `finished` |
| `usage` | what the provider reported: input, output, cache read and cache write tokens. Meaningful only where the call finished. |
+| `model` | the provider model the call ran on, as the runtime named it, on the records of a call. What a token count is reported under. Empty on records landed before it was kept. |
| `parts` | the content |
| `dropped` | what the conversion chose to leave out, with its size and the reason |
diff --git a/internal/adapters/claudecode/convert.go b/internal/adapters/claudecode/convert.go
index fe16afd..c20905e 100644
--- a/internal/adapters/claudecode/convert.go
+++ b/internal/adapters/claudecode/convert.go
@@ -86,6 +86,7 @@ func Convert(src Source, ord, off uint64, payload []byte) *sessiondata.Record {
tur, hasTUR := decodeToolResult(d.ToolUseResult)
rec.ID, rec.Parent = d.UUID, d.ParentUUID
rec.Call, rec.Run = d.Message.ID, d.PromptID
+ rec.Model = d.Message.Model
rec.Continues = d.LogicalParentUUID
rec.Time = d.Timestamp
rec.Trigger = triggerName(triggerOf(&d))
diff --git a/internal/adapters/claudecode/glossary.go b/internal/adapters/claudecode/glossary.go
index 63b7128..18a58d4 100644
--- a/internal/adapters/claudecode/glossary.go
+++ b/internal/adapters/claudecode/glossary.go
@@ -53,6 +53,8 @@ var glossary = model.NewGlossary(Dialect,
model.Term{Unified: model.RoleTool, Native: "tool_use_id", Where: "a tool_result block, a notification, a sidecar"},
model.Term{Unified: model.RoleChild, Native: "toolUseResult.agentId", Where: "a launch result, a journal record"},
model.Term{Unified: model.RoleTime, Native: "timestamp", Where: "record"},
+ model.Term{Unified: model.RoleModel, Native: "message.model", Where: "an assistant record",
+ Note: "<synthetic> on a record the client fabricated"},
model.Term{Unified: model.RoleTrigger, Native: "origin.kind", Where: "record, and attachment.origin.kind"},
// ---- structure the model derived; the runtime records none of it ----
diff --git a/internal/scenario/expect/expect.go b/internal/scenario/expect/expect.go
index 996705c..ce267cf 100644
--- a/internal/scenario/expect/expect.go
+++ b/internal/scenario/expect/expect.go
@@ -752,7 +752,7 @@ type recordView struct {
ID, Parent, Call, Run, Continues, Tool, Child, RecBatch, Label, StartedBy string
From, Time, Trigger string
Flags string
- Usage string
+ Usage, Model string
Parts string
Dropped int
}
@@ -781,7 +781,7 @@ func viewOf(hdr *sessiondata.Header, r *sessiondata.Record) recordView {
ID: r.ID, Parent: r.Parent, Call: r.Call, Run: r.Run, Continues: r.Continues, Tool: r.Tool, Child: r.Child,
RecBatch: r.Batch, Label: r.Label, StartedBy: r.StartedBy,
From: string(r.From), Time: r.Time, Trigger: r.Trigger,
- Flags: strings.Join(flags, ","), Usage: usage, Parts: strings.Join(parts, ";"), Dropped: len(r.Dropped),
+ Flags: strings.Join(flags, ","), Usage: usage, Model: r.Model, Parts: strings.Join(parts, ";"), Dropped: len(r.Dropped),
}
}
@@ -875,7 +875,7 @@ func RecordsWellFormed(root, session string) ([]string, error) {
"from": true, "time": true, "trigger": true, "flags": true,
"id": true, "parent": true, "call": true, "run": true, "continues": true, "tool": true, "child": true,
"batch": true, "label": true, "started_by": true,
- "parts": true, "usage": true, "dropped": true,
+ "parts": true, "usage": true, "model": true, "dropped": true,
}
var out []string
for _, lf := range files {
diff --git a/internal/scenario/write_claudecode.go b/internal/scenario/write_claudecode.go
index 7274ae0..cbb3159 100644
--- a/internal/scenario/write_claudecode.go
+++ b/internal/scenario/write_claudecode.go
@@ -136,6 +136,10 @@ type ccWriter struct {
func ccTime(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
+// ccModel is the model every planned call ran on, written by both writers
+// so a metric per model can be checked across the formats.
+const ccModel = "claude-opus-5"
+
// rec adds the envelope every Claude Code record carries.
func (w *ccWriter) rec(m map[string]any, stream string) string {
for k, v := range map[string]any{
@@ -224,7 +228,7 @@ func (w *ccWriter) event(e *Event) error {
}
w.add(s, w.rec(map[string]any{
"type": "assistant", "uuid": e.ID, "parentUuid": parentOf(e.Parent), "requestId": e.Req, "timestamp": ccTime(e.At),
- "message": map[string]any{"id": e.Call, "type": "message", "role": "assistant", "model": "claude-opus-5",
+ "message": map[string]any{"id": e.Call, "type": "message", "role": "assistant", "model": ccModel,
"stop_reason": stop, "content": []map[string]any{block}, "usage": ccUsage(e.Usage)},
}, s))
case EvResult:
diff --git a/internal/scenario/write_sd.go b/internal/scenario/write_sd.go
index aaebb90..2384277 100644
--- a/internal/scenario/write_sd.go
+++ b/internal/scenario/write_sd.go
@@ -290,7 +290,7 @@ func (w *sdWriter) record(e *Event) *sessiondata.Record {
r.From, r.Flags = sessiondata.FromRuntime, []string{"injected"}
r.Parts = []sessiondata.Part{textPart(e.Text)}
case EvFragment:
- r.From, r.Call = sessiondata.FromAgent, e.Call
+ r.From, r.Call, r.Model = sessiondata.FromAgent, e.Call, ccModel
u := e.Usage
r.Usage = &sessiondata.Usage{Input: u.In, Output: u.Out, CacheRead: u.CacheRead, CacheWrite: u.CacheWrite}
if e.Last {
@@ -341,7 +341,7 @@ func (w *sdWriter) record(e *Event) *sessiondata.Record {
r.From, r.Trigger, r.Tool, r.Child = sessiondata.FromExternal, model.TriggerNotification, e.NoticeTool, e.NoticeChild
r.Parts = []sessiondata.Part{textPart(e.Text)}
case EvSynthetic:
- r.From, r.Call, r.Flags = sessiondata.FromAgent, e.Call, []string{"synthetic"}
+ r.From, r.Call, r.Flags, r.Model = sessiondata.FromAgent, e.Call, []string{"synthetic"}, "<synthetic>"
r.Usage = &sessiondata.Usage{}
r.Parts = []sessiondata.Part{textPart(e.Text)}
case EvBoundary:
diff --git a/pkg/model/term.go b/pkg/model/term.go
index 5d63328..0dbb070 100644
--- a/pkg/model/term.go
+++ b/pkg/model/term.go
@@ -127,7 +127,7 @@ func Vocabulary() []string {
RelInSegment, RelRetries, RelCancels, RelInputOf,
// the roles a record plays, which a reader meets in a reference
RoleID, RoleParent, RoleCall, RoleRun, RoleBatch, RoleStream,
- RoleContinues, RoleTool, RoleChild, RoleTime, RoleTrigger,
+ RoleContinues, RoleTool, RoleChild, RoleTime, RoleTrigger, RoleModel,
// qualification
ExactUnique, ExactAmbiguous, StrongInference, WeakInference, Unresolved, Conflict,
ContentAvailable, ContentRedacted, ContentOmitted, ContentHashOnly,
@@ -151,4 +151,5 @@ const (
RoleChild = "child"
RoleTime = "time"
RoleTrigger = "trigger"
+ RoleModel = "model"
)
diff --git a/pkg/sessiondata/fields.go b/pkg/sessiondata/fields.go
index 72c71bf..5e01d15 100644
--- a/pkg/sessiondata/fields.go
+++ b/pkg/sessiondata/fields.go
@@ -48,6 +48,7 @@ func Fields() map[string]string {
"from": "who produced the record: the agent, something outside it, or the runtime",
"flags": "short facts about the record that do not belong in a field of their own",
"usage": "tokens the provider reported for this call",
+ "model": "the provider model the call ran on, as the runtime named it",
"parts": "the pieces of the record, in the order they were written",
"dropped": "what was left out of this record, how big it was, and why",
diff --git a/pkg/sessiondata/sessiondata.go b/pkg/sessiondata/sessiondata.go
index 5c71c64..a11ba8e 100644
--- a/pkg/sessiondata/sessiondata.go
+++ b/pkg/sessiondata/sessiondata.go
@@ -213,6 +213,12 @@ type Record struct {
// still carries a usage block, and its output count is a streaming stub of
// a few tokens.
Usage *Usage `json:"usage,omitempty"`
+ // Model is the provider model a call ran on, as the runtime named it, on
+ // the records of that call. It is what a token count is reported under,
+ // so a metric per model can be produced from the landed data alone.
+ // Empty on records landed before it was kept, and on records that are
+ // not a call.
+ Model string `json:"model,omitempty"`
Parts []Part `json:"parts,omitempty"`