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"`