From 4daf7efabaa376c91ea953b943faad7ec5df538f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 18 Oct 2026 21:24:54 +0000
Subject: [PATCH 1/4] Initial plan
From 62f72e63df9781e2779c02f8ac208db4e260df68 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 18 Oct 2026 21:39:52 +0000
Subject: [PATCH 2/4] Preserve AWF model inventories with null endpoints and
reasoning metadata
Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>
---
...erve-awf-model-inventory-null-endpoints.md | 5 +
.prettierignore | 3 +
.../setup/js/test_data/awf_reflect/README.md | 17 ++
.../run-29812420778-legacy-awf-reflect.json | 1 +
.../run-38995410124-awf-reflect.json | 1 +
actions/setup/js/types/unified_session.d.ts | 10 +-
actions/setup/js/unified_session.test.cjs | 4 +-
actions/setup/js/unified_session_payload.cjs | 61 ++--
.../setup/js/unified_session_payload.test.cjs | 280 +++++++++++++++++-
.../schemas/unified-session.schema.json | 18 +-
.../unified-agent-session-specification.md | 58 +++-
11 files changed, 426 insertions(+), 32 deletions(-)
create mode 100644 .changeset/preserve-awf-model-inventory-null-endpoints.md
create mode 100644 actions/setup/js/test_data/awf_reflect/README.md
create mode 100644 actions/setup/js/test_data/awf_reflect/run-29812420778-legacy-awf-reflect.json
create mode 100644 actions/setup/js/test_data/awf_reflect/run-38995410124-awf-reflect.json
diff --git a/.changeset/preserve-awf-model-inventory-null-endpoints.md b/.changeset/preserve-awf-model-inventory-null-endpoints.md
new file mode 100644
index 00000000000..0c076bc4197
--- /dev/null
+++ b/.changeset/preserve-awf-model-inventory-null-endpoints.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": patch
+---
+
+Keep AWF model inventories in unified sessions when an unconfigured endpoint reports null `models` or `routing_models`. Null lists are now omitted as unknown instead of becoming empty arrays or rejecting the whole snapshot, and endpoint `configured`, routing `supportedReasoningEfforts`, and `contextWindowTokens` are retained (unified session specification 2.0.0).
diff --git a/.prettierignore b/.prettierignore
index c138c3b0e6f..b70ef76cf4b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -21,3 +21,6 @@ schemas/logs-jsonl.schema.json
.beads/
.github/aw/
+
+# Recorded byte-exact AWF reflection fixtures (SHA-256 pinned)
+actions/setup/js/test_data/awf_reflect/*.json
diff --git a/actions/setup/js/test_data/awf_reflect/README.md b/actions/setup/js/test_data/awf_reflect/README.md
new file mode 100644
index 00000000000..5ed948712c7
--- /dev/null
+++ b/actions/setup/js/test_data/awf_reflect/README.md
@@ -0,0 +1,17 @@
+# Recorded AWF `/reflect` fixtures
+
+These files are unchanged byte-for-byte copies of `sandbox/firewall/awf-reflect.json`
+from public `github/gh-aw` workflow-run artifacts. They are excluded from
+prettier so that their SHA-256 digests stay verifiable. Tests derive labelled
+pending-fetch and malformed variants in memory; they never edit these files.
+
+| File | Source run | Artifact | SHA-256 | Shape |
+| --- | --- | --- | --- | --- |
+| `run-38995410124-awf-reflect.json` | [smoke-claude-on-copilot 38995410124](https://github.com/github/gh-aw/actions/runs/38995410124), commit `12ba3ffcbaf5760b6094f52d8f7d61938e4e951e`, 2026-10-17 | `agent` (12087091651) | `9d59511015584d359b4159737d1d44ed12ad9426a1690e62148ab53b7543648a` | Five endpoints; configured Copilot with 40 models and 40 `routing_models`; four unconfigured endpoints with null `models` and `routing_models` |
+| `run-29812420778-legacy-awf-reflect.json` | [Smoke Copilot 29812420778](https://github.com/github/gh-aw/actions/runs/29812420778), commit `fed7b8398300613d6f79474d75d3ffcdc6665d18`, 2026-07-21 | `agent` (8488185681) | `da2570969e0d62bd10195f029b5472f750404b515784d8c68e9b267e0975b410` | Older AWF reflection without `routing_models`; configured Copilot with 36 models and four unconfigured endpoints with null `models` |
+
+The legacy file is the oldest retained public reflection found. It is not the
+source of a `pkg/cli/testdata/model_routing_golden/` case: those source runs are
+either private or expired (for example, run 37886511291 returns HTTP 404). Every
+AWF `/reflect` version since its introduction supplies endpoint `configured`, so
+no recorded reflection without `configured` exists to pin.
diff --git a/actions/setup/js/test_data/awf_reflect/run-29812420778-legacy-awf-reflect.json b/actions/setup/js/test_data/awf_reflect/run-29812420778-legacy-awf-reflect.json
new file mode 100644
index 00000000000..edd844a5ee9
--- /dev/null
+++ b/actions/setup/js/test_data/awf_reflect/run-29812420778-legacy-awf-reflect.json
@@ -0,0 +1 @@
+{"endpoints":[{"provider":"openai","port":10000,"base_url":"http://api-proxy:10000","configured":false,"models":null,"models_url":"http://api-proxy:10000/v1/models"},{"provider":"anthropic","port":10001,"base_url":"http://api-proxy:10001","configured":false,"models":null,"models_url":"http://api-proxy:10001/v1/models"},{"provider":"copilot","port":10002,"base_url":"http://api-proxy:10002","configured":true,"models":["claude-haiku-4.5","claude-opus-4.5","claude-opus-4.6","claude-opus-4.7","claude-opus-4.8","claude-sonnet-4.5","claude-sonnet-4.6","claude-sonnet-5","gemini-2.5-pro","gemini-3-flash-preview","gpt-3.5-turbo","gpt-3.5-turbo-0613","gpt-4","gpt-4-0613","gpt-4-o-preview","gpt-4.1","gpt-4.1-2025-04-14","gpt-4o","gpt-4o-2024-05-13","gpt-4o-2024-08-06","gpt-4o-2024-11-20","gpt-4o-mini","gpt-4o-mini-2024-07-18","gpt-5-mini","gpt-5.3-codex","gpt-5.4","gpt-5.4-mini","gpt-5.5","gpt-5.6-luna","gpt-5.6-sol","gpt-5.6-terra","mai-code-1-flash-picker","text-embedding-3-small","text-embedding-3-small-inference","text-embedding-ada-002","trajectory-compaction"],"models_url":"http://api-proxy:10002/models"},{"provider":"gemini","port":10003,"base_url":"http://api-proxy:10003","configured":false,"models":null,"models_url":"http://api-proxy:10003/v1beta/models"},{"provider":"vertex","port":10004,"base_url":"http://api-proxy:10004","configured":false,"models":null,"models_url":null}],"models_fetch_complete":true,"model_aliases":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex","kimi"],"copilot":["agent"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"gpt-5.6":["copilot/gpt-5.6*","openai/gpt-5.6*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["fable","sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mai-code-1-flash-picker":["copilot/MAI-Code-1-Flash-picker*","copilot/mai-code-1-flash-picker*","openai/MAI-Code-1-Flash-picker*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-5*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*","anthropic/*sonnet-5*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"]},"model_fallback":{"enabled":true,"strategy":"middle_power","excludeEngines":[]},"model_fallback_effective":{"openai":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false},"anthropic":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false},"copilot":{"enabled":false,"strategy":"middle_power","excludeEngines":[],"suppressed":true,"suppression_reason":"copilot_standard_authoritative"},"gemini":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false},"vertex":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false}},"ai_credits":{"total":225.64295,"by_model":{"gpt-5.4-2026-03-05":{"input_credits":188.21525,"cached_input_credits":17.0112,"cache_write_credits":0,"output_credits":20.4165,"total":225.64295}}},"runs":{"enabled":true,"max_runs":500,"invocation_count":18,"remaining_runs":482},"cache_misses":{"enabled":true,"max_cache_misses":5,"consecutive_cache_misses":0,"remaining_cache_misses":5},"permission_denied":{"enabled":false,"max_permission_denied":null,"denied_count":0},"model_api_mapping":{"available":true,"last_updated":"2026-07-16T06:03:36Z","providers":["openai","anthropic"],"models":{"openai":{"endpoints":{"responses":{"path":"/v1/responses","description":"Newer unified API for agentic, tool-using, and multimodal workflows. Required for GPT-5+ models; some older models (o1, o3, gpt-4o, gpt-4.1) support it alongside chat/completions."},"chat_completions":{"path":"/v1/chat/completions","description":"Legacy conversational API. Being deprecated for newer models by end of 2026."}},"models":[{"family":"gpt-5.6","patterns":["gpt-5.6*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Includes gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna per OpenAI SDK ChatModel."},{"family":"gpt-5.5","patterns":["gpt-5.5*"],"endpoints":["responses"],"notes":"Responses API only. Not present in OpenAI OpenAPI spec v2.3.0; reserved for future gpt-5.5 family models."},{"family":"gpt-5.4","patterns":["gpt-5.4*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints on OpenAI. Includes gpt-5.4-mini, gpt-5.4-nano. NOTE: gpt-5.4-mini is not accessible via /chat/completions on the Copilot API (responses-only via Copilot); the api-proxy handles this by falling back to the next alias candidate."},{"family":"gpt-5.3","patterns":["gpt-5.3*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Only gpt-5.3-chat-latest confirmed in OpenAPI spec; gpt-5.3-codex variants not present."},{"family":"gpt-5.2","patterns":["gpt-5.2*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Includes gpt-5.2-pro."},{"family":"gpt-5.1-codex-max","patterns":["gpt-5.1-codex-max"],"endpoints":["responses"],"notes":"Responses API only (ResponsesOnlyModel)."},{"family":"gpt-5.1","patterns":["gpt-5.1*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints for gpt-5.1, gpt-5.1-codex, gpt-5.1-mini."},{"family":"gpt-5-codex/pro","patterns":["gpt-5-codex","gpt-5-pro","gpt-5-pro-*"],"endpoints":["responses"],"notes":"Responses API only for gpt-5-codex and gpt-5-pro (ResponsesOnlyModel). Includes dated variants like gpt-5-pro-2025-10-06."},{"family":"gpt-5","patterns":["gpt-5","gpt-5-*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints for gpt-5, gpt-5-mini, gpt-5-nano."},{"family":"o4-mini-deep-research","patterns":["o4-mini-deep-research","o4-mini-deep-research-*"],"endpoints":["responses"],"notes":"Responses API only (ResponsesOnlyModel). Includes dated variants like o4-mini-deep-research-2025-06-26."},{"family":"o4","patterns":["o4*"],"endpoints":["chat_completions","responses"],"notes":"Reasoning model. o4-mini supports both endpoints (ModelIdsShared)."},{"family":"o3-pro/deep-research","patterns":["o3-pro","o3-pro-*","o3-deep-research","o3-deep-research-*"],"endpoints":["responses"],"notes":"Responses API only. o3-pro and o3-deep-research are in ResponsesModel but not ChatModel."},{"family":"o3","patterns":["o3*"],"endpoints":["chat_completions","responses"],"notes":"Reasoning model. o3, o3-mini support both endpoints. o3-pro/deep-research use responses only (covered by separate entry above)."},{"family":"o1-pro","patterns":["o1-pro","o1-pro-*"],"endpoints":["responses"],"notes":"Responses API only. o1-pro is in ResponsesModel but not ChatModel."},{"family":"o1","patterns":["o1*"],"endpoints":["chat_completions","responses"],"notes":"Reasoning model. o1, o1-mini, o1-preview support both endpoints. o1-pro uses responses only (covered by separate entry above)."},{"family":"computer-use-preview","patterns":["computer-use-preview*"],"endpoints":["responses"],"notes":"Responses API only. Specialized model for computer use tasks."},{"family":"codex-mini","patterns":["codex-mini*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. codex-mini-latest is in ChatModel."},{"family":"chatgpt-4o-latest","patterns":["chatgpt-4o-latest"],"endpoints":["chat_completions","responses"],"notes":"Rolling alias for the latest ChatGPT-optimized GPT-4o model. Supports both endpoints (ModelIdsShared). Not matched by gpt-4o* pattern due to chatgpt- prefix."},{"family":"gpt-4o","patterns":["gpt-4o*"],"endpoints":["chat_completions","responses"],"notes":"Multimodal. Supports both endpoints. Includes audio and search preview variants."},{"family":"gpt-4.1","patterns":["gpt-4.1*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Includes gpt-4.1-mini, gpt-4.1-nano."},{"family":"gpt-4.5","patterns":["gpt-4.5*"],"endpoints":["chat_completions"],"notes":"Preview model. Chat completions only."},{"family":"gpt-4-turbo","patterns":["gpt-4-turbo*"],"endpoints":["chat_completions","responses"],"notes":"Legacy. In ModelIdsShared per OpenAI spec v2.3.0; supports both endpoints."},{"family":"gpt-4","patterns":["gpt-4","gpt-4-*"],"endpoints":["chat_completions","responses"],"notes":"Legacy. In ModelIdsShared per OpenAI spec v2.3.0; supports both endpoints."},{"family":"gpt-3.5-turbo","patterns":["gpt-3.5-turbo*"],"endpoints":["chat_completions","responses"],"notes":"Legacy. In ModelIdsShared per OpenAI spec v2.3.0; supports both endpoints. Deprecated."}]},"anthropic":{"endpoints":{"messages":{"path":"/v1/messages","description":"Unified Messages API for all Claude models. Supports tool use, streaming, extended thinking, and vision."}},"models":[{"family":"claude-fable-5","patterns":["claude-fable-5*"],"endpoints":["messages"],"notes":"Latest flagship. 1M token context."},{"family":"claude-mythos-5","patterns":["claude-mythos-5*","claude-mythos-preview*"],"endpoints":["messages"],"notes":"Invitation-only (Project Glasswing). Limited availability. 1M token context."},{"family":"claude-sonnet-5","patterns":["claude-sonnet-5*"],"endpoints":["messages"],"notes":"Best combination of speed and intelligence. 1M token context."},{"family":"claude-opus-4","patterns":["claude-opus-4*"],"endpoints":["messages"],"notes":"Includes claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-opus-4-5, claude-opus-4-1. 1M token context."},{"family":"claude-sonnet-4","patterns":["claude-sonnet-4*"],"endpoints":["messages"],"notes":"Includes claude-sonnet-4-6, claude-sonnet-4-5. Production workhorse. 1M token context."},{"family":"claude-haiku-4","patterns":["claude-haiku-4*"],"endpoints":["messages"],"notes":"Includes claude-haiku-4-5. Fast/cheap. 200K token context."},{"family":"claude-3.5-sonnet","patterns":["claude-3-5-sonnet*","claude-3.5-sonnet*"],"endpoints":["messages"],"notes":"Previous generation. 200K token context."},{"family":"claude-3.5-haiku","patterns":["claude-3-5-haiku*","claude-3.5-haiku*"],"endpoints":["messages"],"notes":"Previous generation fast model."},{"family":"claude-3.7-sonnet","patterns":["claude-3-7-sonnet*","claude-3.7-sonnet*"],"endpoints":["messages"],"notes":"Previous generation extended thinking model. 200K token context."},{"family":"claude-3-opus","patterns":["claude-3-opus*"],"endpoints":["messages"],"notes":"Previous generation. 200K token context."}]}},"error":null}}
\ No newline at end of file
diff --git a/actions/setup/js/test_data/awf_reflect/run-38995410124-awf-reflect.json b/actions/setup/js/test_data/awf_reflect/run-38995410124-awf-reflect.json
new file mode 100644
index 00000000000..1baa9496d0e
--- /dev/null
+++ b/actions/setup/js/test_data/awf_reflect/run-38995410124-awf-reflect.json
@@ -0,0 +1 @@
+{"endpoints":[{"provider":"openai","port":10000,"base_url":"http://api-proxy:10000","configured":false,"models":null,"model_metadata":null,"automatic_model_selection":{"model":"auto","constraint_field":"model_provider","model_providers":["anthropic","openai","google"],"preserves_backend":true,"requires_advertised_endpoints":true,"candidates":{"anthropic":{"messages":[],"chat/completions":[],"responses":[]},"openai":{"messages":[],"chat/completions":[],"responses":[]},"google":{"messages":[],"chat/completions":[],"responses":[]}}},"routing_models":null,"models_url":"http://api-proxy:10000/v1/models"},{"provider":"anthropic","port":10001,"base_url":"http://api-proxy:10001","configured":false,"models":null,"model_metadata":null,"automatic_model_selection":{"model":"auto","constraint_field":"model_provider","model_providers":["anthropic","openai","google"],"preserves_backend":true,"requires_advertised_endpoints":true,"candidates":{"anthropic":{"messages":[],"chat/completions":[],"responses":[]},"openai":{"messages":[],"chat/completions":[],"responses":[]},"google":{"messages":[],"chat/completions":[],"responses":[]}}},"routing_models":null,"models_url":"http://api-proxy:10001/v1/models"},{"provider":"copilot","port":10002,"base_url":"http://api-proxy:10002","configured":true,"models":["claude-haiku-4.5","claude-haiku-5.5","claude-opus-5.5","claude-sonnet-5","claude-sonnet-5.5","gpt-3.5-turbo","gpt-3.5-turbo-0613","gpt-4","gpt-4-0613","gpt-4-o-preview","gpt-4.1","gpt-4.1-2025-04-14","gpt-41-copilot","gpt-4o","gpt-4o-2024-05-13","gpt-4o-2024-08-06","gpt-4o-2024-11-20","gpt-4o-mini","gpt-4o-mini-2024-07-18","gpt-5-mini","gpt-5.3-codex","gpt-5.4","gpt-5.4-mini","gpt-5.5","gpt-5.6-luna","gpt-5.6-luna-utility","gpt-5.6-sol","gpt-5.6-terra","gpt-6-astra","gpt-6-luna","gpt-6-sol","gpt-6.1-sol","grok-4.5","grok-4.6","grok-4.7","mai-code-1.1-flash","text-embedding-3-small","text-embedding-3-small-inference","text-embedding-ada-002","trajectory-compaction"],"model_metadata":[{"id":"claude-haiku-4.5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":1,"output":5,"cachedInput":0.1,"cacheWrite":1.25}}},{"id":"claude-haiku-5.5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":0.1,"output":0.5,"cachedInput":0.01,"cacheWrite":0.12,"threshold":100000},"long_context":{"input":0.5,"output":2.5,"cachedInput":0.05,"cacheWrite":0.62,"threshold":936000}}},{"id":"claude-opus-5.5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":4,"output":20,"cachedInput":0.2,"cacheWrite":5,"threshold":200000},"long_context":{"input":4,"output":20,"cachedInput":0.2,"cacheWrite":5,"threshold":1000000}}},{"id":"claude-sonnet-5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2,"output":10,"cachedInput":0.2,"cacheWrite":2.5,"threshold":200000},"long_context":{"input":2,"output":10,"cachedInput":0.2,"cacheWrite":2.5,"threshold":936000}}},{"id":"claude-sonnet-5.5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2,"output":10,"cachedInput":0.1,"cacheWrite":2.5,"threshold":200000},"long_context":{"input":2,"output":10,"cachedInput":0.1,"cacheWrite":2.5,"threshold":936000}}},{"id":"gpt-3.5-turbo","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-3.5-turbo-0613","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2.5,"output":10,"cachedInput":1.25,"cacheWrite":0}}},{"id":"gpt-4-0613","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2.5,"output":10,"cachedInput":1.25,"cacheWrite":0}}},{"id":"gpt-4-o-preview","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4.1","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4.1-2025-04-14","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-41-copilot","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2,"output":8,"cachedInput":0.5,"cacheWrite":0}}},{"id":"gpt-4o","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4o-2024-05-13","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4o-2024-08-06","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4o-2024-11-20","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4o-mini","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-4o-mini-2024-07-18","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-5-mini","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":0.25,"output":2,"cachedInput":0.02,"cacheWrite":0}}},{"id":"gpt-5.3-codex","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":1.75,"output":14,"cachedInput":0.17,"cacheWrite":0,"threshold":272000}}},{"id":"gpt-5.4","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2.5,"output":15,"cachedInput":0.25,"cacheWrite":0,"threshold":272000},"long_context":{"input":5,"output":22.5,"cachedInput":0.5,"cacheWrite":0,"threshold":922000}}},{"id":"gpt-5.4-mini","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":0.75,"output":4.5,"cachedInput":0.07,"cacheWrite":0,"threshold":272000}}},{"id":"gpt-5.5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":5,"output":30,"cachedInput":0.5,"cacheWrite":0,"threshold":272000},"long_context":{"input":10,"output":45,"cachedInput":1,"cacheWrite":0,"threshold":922000}}},{"id":"gpt-5.6-luna","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":0.2,"output":1.2,"cachedInput":0.02,"cacheWrite":0.25,"threshold":200000},"long_context":{"input":0.4,"output":1.8,"cachedInput":0.04,"cacheWrite":0.5,"threshold":922000}}},{"id":"gpt-5.6-luna-utility","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"gpt-5.6-sol","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":4,"output":20,"cachedInput":0.4,"cacheWrite":5,"threshold":272000},"long_context":{"input":8,"output":30,"cachedInput":0.8,"cacheWrite":10,"threshold":922000}}},{"id":"gpt-5.6-terra","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2,"output":12,"cachedInput":0.2,"cacheWrite":2.5,"threshold":272000},"long_context":{"input":4,"output":18,"cachedInput":0.4,"cacheWrite":5,"threshold":922000}}},{"id":"gpt-6-astra","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":10,"output":50,"cachedInput":1,"cacheWrite":12.5,"threshold":272000},"long_context":{"input":20,"output":75,"cachedInput":2,"cacheWrite":25,"threshold":1050000}}},{"id":"gpt-6-luna","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":0.1,"output":0.5,"cachedInput":0.01,"cacheWrite":0.12,"threshold":272000},"long_context":{"input":0.2,"output":0.75,"cachedInput":0.02,"cacheWrite":0.25,"threshold":872000}}},{"id":"gpt-6-sol","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2,"output":10,"cachedInput":0.2,"cacheWrite":2.5,"threshold":272000},"long_context":{"input":4,"output":15,"cachedInput":0.4,"cacheWrite":5,"threshold":872000}}},{"id":"gpt-6.1-sol","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":2,"output":10,"cachedInput":0.1,"cacheWrite":2.5,"threshold":272000},"long_context":{"input":4,"output":15,"cachedInput":0.2,"cacheWrite":5,"threshold":922000}}},{"id":"grok-4.5","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","wire_api":"responses","pricing":{"default":{"input":2,"output":6,"cachedInput":0.5,"cacheWrite":0,"threshold":200000},"long_context":{"input":4,"output":12,"cachedInput":1,"cacheWrite":0,"threshold":500000}}},{"id":"grok-4.6","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","wire_api":"responses","pricing":{"default":{"input":2,"output":6,"cachedInput":0.5,"cacheWrite":0,"threshold":200000},"long_context":{"input":4,"output":12,"cachedInput":1,"cacheWrite":0,"threshold":500000}}},{"id":"grok-4.7","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","wire_api":"responses","pricing":{"default":{"input":2,"output":6,"cachedInput":0.5,"cacheWrite":0,"threshold":200000},"long_context":{"input":4,"output":12,"cachedInput":1,"cacheWrite":0,"threshold":500000}}},{"id":"mai-code-1.1-flash","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","pricing":{"default":{"input":0.2,"output":1.2,"cachedInput":0.02,"cacheWrite":0.25,"threshold":128000}}},{"id":"text-embedding-3-small","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"text-embedding-3-small-inference","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"text-embedding-ada-002","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01"},{"id":"trajectory-compaction","source":"provider","observed_at":"2026-10-17T02:35:49.726Z","api_version":"2026-07-01","wire_api":"completions"}],"automatic_model_selection":{"model":"auto","constraint_field":"model_provider","model_providers":["anthropic","openai","google"],"preserves_backend":true,"requires_advertised_endpoints":true,"candidates":{"anthropic":{"messages":["claude-sonnet-5.5","claude-sonnet-5","claude-opus-5.5","claude-haiku-5.5","claude-haiku-4.5"],"chat/completions":["claude-sonnet-5.5","claude-sonnet-5","claude-opus-5.5","claude-haiku-5.5","claude-haiku-4.5"],"responses":[]},"openai":{"messages":[],"chat/completions":["gpt-5.4","gpt-5-mini"],"responses":["gpt-6.1-sol","gpt-6-astra","gpt-6-luna","gpt-6-sol","gpt-5.6-luna","gpt-5.6-sol","gpt-5.6-terra","gpt-5.5","gpt-5.4","gpt-5.4-mini","gpt-5.3-codex","gpt-5-mini"]},"google":{"messages":[],"chat/completions":[],"responses":[]}}},"routing_models":[{"model_id":"claude-haiku-4.5","source":"provider","supported_endpoints":["/chat/completions","/v1/messages"],"supported_reasoning_efforts":[],"context_window_tokens":200000,"candidate_metadata_complete":true},{"model_id":"claude-haiku-5.5","source":"provider","supported_endpoints":["/v1/messages","/chat/completions"],"supported_reasoning_efforts":["low","medium","high","xhigh","max"],"context_window_tokens":1000000,"candidate_metadata_complete":true},{"model_id":"claude-opus-5.5","source":"provider","supported_endpoints":["/v1/messages","/chat/completions"],"supported_reasoning_efforts":["low","medium","high","xhigh","max"],"context_window_tokens":1000000,"candidate_metadata_complete":true},{"model_id":"claude-sonnet-5","source":"provider","supported_endpoints":["/v1/messages","/chat/completions"],"supported_reasoning_efforts":["low","medium","high","xhigh","max"],"context_window_tokens":1000000,"candidate_metadata_complete":true},{"model_id":"claude-sonnet-5.5","source":"provider","supported_endpoints":["/v1/messages","/chat/completions"],"supported_reasoning_efforts":["low","medium","high","xhigh","max"],"context_window_tokens":1000000,"candidate_metadata_complete":true},{"model_id":"gpt-3.5-turbo","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":16384,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-3.5-turbo-0613","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":16384,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":32768,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4-0613","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":32768,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4-o-preview","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4.1","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4.1-2025-04-14","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-41-copilot","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":null,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4o","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4o-2024-05-13","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4o-2024-08-06","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4o-2024-11-20","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4o-mini","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-4o-mini-2024-07-18","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":128000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-5-mini","source":"provider","supported_endpoints":["/chat/completions","/responses","ws:/responses"],"supported_reasoning_efforts":["low","medium","high"],"context_window_tokens":264000,"candidate_metadata_complete":true},{"model_id":"gpt-5.3-codex","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["low","medium","high","xhigh"],"context_window_tokens":400000,"candidate_metadata_complete":true},{"model_id":"gpt-5.4","source":"provider","supported_endpoints":["/responses","/chat/completions","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"gpt-5.4-mini","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh"],"context_window_tokens":400000,"candidate_metadata_complete":true},{"model_id":"gpt-5.5","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"gpt-5.6-luna","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"gpt-5.6-luna-utility","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1050000,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"gpt-5.6-sol","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"gpt-5.6-terra","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"gpt-6-astra","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["low","medium","high","xhigh","max"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"gpt-6-luna","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1000000,"candidate_metadata_complete":true},{"model_id":"gpt-6-sol","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1000000,"candidate_metadata_complete":true},{"model_id":"gpt-6.1-sol","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["none","low","medium","high","xhigh","max"],"context_window_tokens":1050000,"candidate_metadata_complete":true},{"model_id":"grok-4.5","source":"provider","supported_endpoints":["/responses"],"supported_reasoning_efforts":["low","medium","high"],"context_window_tokens":500000,"candidate_metadata_complete":true},{"model_id":"grok-4.6","source":"provider","supported_endpoints":["/responses"],"supported_reasoning_efforts":["low","medium","high","xhigh"],"context_window_tokens":500000,"candidate_metadata_complete":true},{"model_id":"grok-4.7","source":"provider","supported_endpoints":["/responses"],"supported_reasoning_efforts":["low","medium","high","xhigh"],"context_window_tokens":500000,"candidate_metadata_complete":true},{"model_id":"mai-code-1.1-flash","source":"provider","supported_endpoints":["/responses","ws:/responses"],"supported_reasoning_efforts":["low","medium","high"],"context_window_tokens":256000,"candidate_metadata_complete":true},{"model_id":"text-embedding-3-small","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":null,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"text-embedding-3-small-inference","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":null,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"text-embedding-ada-002","source":"provider","supported_endpoints":[],"supported_reasoning_efforts":[],"context_window_tokens":null,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"},{"model_id":"trajectory-compaction","source":"provider","supported_endpoints":["/chat/completions"],"supported_reasoning_efforts":[],"context_window_tokens":262144,"candidate_metadata_complete":false,"candidate_metadata_reason":"Model is not enabled in the Copilot model picker"}],"models_url":"http://api-proxy:10002/models","credential_kind":"fine_grained_pat","selected_scheme":"Bearer","inference_credential_source":"github_token","inference_selected_scheme":"Bearer","integration_id_source":"default"},{"provider":"gemini","port":10003,"base_url":"http://api-proxy:10003","configured":false,"models":null,"model_metadata":null,"automatic_model_selection":{"model":"auto","constraint_field":"model_provider","model_providers":["anthropic","openai","google"],"preserves_backend":true,"requires_advertised_endpoints":true,"candidates":{"anthropic":{"messages":[],"chat/completions":[],"responses":[]},"openai":{"messages":[],"chat/completions":[],"responses":[]},"google":{"messages":[],"chat/completions":[],"responses":[]}}},"routing_models":null,"models_url":"http://api-proxy:10003/v1beta/models"},{"provider":"vertex","port":10004,"base_url":"http://api-proxy:10004","configured":false,"models":null,"model_metadata":null,"automatic_model_selection":{"model":"auto","constraint_field":"model_provider","model_providers":["anthropic","openai","google"],"preserves_backend":true,"requires_advertised_endpoints":true,"candidates":{"anthropic":{"messages":[],"chat/completions":[],"responses":[]},"openai":{"messages":[],"chat/completions":[],"responses":[]},"google":{"messages":[],"chat/completions":[],"responses":[]}}},"routing_models":null,"models_url":null}],"models_fetch_complete":true,"model_aliases":{"agent":["sonnet-6x","gpt-6","gpt-5.4","gpt-5.5","gpt-5.6","gpt-5.3","gemini-pro","any"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"auto":["copilot/auto","large"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex","kimi"],"copilot":["agent"],"detection":["small"],"evals":["small"],"gemini":["agent"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"gpt-5.6":["copilot/gpt-5.6*","openai/gpt-5.6*"],"gpt-6":["copilot/gpt-6*","openai/gpt-6*"],"grok":["copilot/*grok*","openai/*grok*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-6","gpt-5-pro","gpt-5","gemini-pro"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-5*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*","anthropic/*sonnet-5*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"]},"model_fallback":{"enabled":true,"strategy":"middle_power","excludeEngines":[]},"model_fallback_effective":{"openai":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false},"anthropic":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false},"copilot":{"enabled":false,"strategy":"middle_power","excludeEngines":[],"suppressed":true,"suppression_reason":"copilot_standard_authoritative"},"gemini":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false},"vertex":{"enabled":true,"strategy":"middle_power","excludeEngines":[],"suppressed":false}},"ai_credits":{"total":0.147373,"max_ai_credits":1000,"effective_max_ai_credits":1000,"percent_used":0.01,"thresholds_crossed":[],"thresholds_pending":[],"by_model":{"claude-haiku-5-5":{"input_credits":0.00006,"cached_input_credits":0.008873,"cache_write_credits":0.11154,"output_credits":0.0269,"total":0.147373,"pricing_source":"provider","pricing_tier":"default","accounting_policy":"concrete_model","fallback_pricing_used":false,"dynamic_selector":null}}},"time_steering":{"enabled":true,"timeout_minutes":10,"started_at_ms":1792204554932,"percent_elapsed":1.26,"thresholds_crossed":[],"thresholds_pending":[]},"runs":{"enabled":true,"max_runs":500,"invocation_count":2,"remaining_runs":498},"cache_misses":{"enabled":true,"max_cache_misses":5,"consecutive_cache_misses":0,"remaining_cache_misses":5},"permission_denied":{"enabled":false,"max_permission_denied":null,"denied_count":0},"model_api_mapping":{"available":true,"last_updated":"2026-10-08T07:12:00Z","providers":["openai","anthropic"],"models":{"openai":{"endpoints":{"responses":{"path":"/v1/responses","description":"Newer unified API for agentic, tool-using, and multimodal workflows. Required for GPT-5+ models; some older models (o1, o3, gpt-4o, gpt-4.1) support it alongside chat/completions."},"chat_completions":{"path":"/v1/chat/completions","description":"Legacy conversational API. Being deprecated for newer models by end of 2026."}},"models":[{"family":"gpt-6-astra","patterns":["gpt-6-astra*"],"endpoints":["chat_completions","responses"],"notes":"Newest flagship model (Trusted Access Program rollout). Supports Chat Completions and Responses APIs."},{"family":"gpt-6-sol","patterns":["gpt-6-sol*"],"endpoints":["chat_completions","responses"],"notes":"GPT-6 tier balancing intelligence and cost. Supports both Chat Completions and Responses APIs per official model page."},{"family":"gpt-6.1-sol","patterns":["gpt-6.1-sol*"],"endpoints":["chat_completions","responses"],"notes":"GPT-6.1 tier balancing intelligence and cost (successor to gpt-6-sol). Supports both Chat Completions and Responses APIs, consistent with other GPT-6 tiers."},{"family":"gpt-6-luna","patterns":["gpt-6-luna*"],"endpoints":["chat_completions","responses"],"notes":"GPT-6 tier for cost-sensitive, high-volume use. Supports both Chat Completions and Responses APIs per official model page."},{"family":"gpt-daybreak","patterns":["gpt-daybreak-blue-latest","gpt-daybreak-red-latest"],"endpoints":["responses"],"notes":"Responses API only (ResponsesModel-only Literal). Experimental/preview aliases not present in ChatModel."},{"family":"gpt-rosalind-research","patterns":["gpt-rosalind-research*"],"endpoints":["responses"],"notes":"Life sciences reasoning model, Trusted Access Program (approved organizations only). Responses API only, consistent with other restricted-access specialized models (gpt-daybreak-*, gpt-5.6-cyber). Billing begins October 5, 2026."},{"family":"gpt-5.6-cyber","patterns":["gpt-5.6-cyber*"],"endpoints":["responses"],"notes":"Responses API only (ResponsesModel-only Literal, not in ChatModel). More specific than gpt-5.6* pattern below."},{"family":"gpt-5.6","patterns":["gpt-5.6*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Includes gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna per OpenAI SDK ChatModel. gpt-5.6-cyber is responses-only (see separate entry above)."},{"family":"gpt-5.5-pro","patterns":["gpt-5.5-pro","gpt-5.5-pro-*"],"endpoints":["responses"],"notes":"Responses API only (ResponsesOnlyModel per OpenAI OpenAPI spec). More specific than gpt-5.5* pattern below. Includes dated variants like gpt-5.5-pro-2026-04-23."},{"family":"gpt-5.5","patterns":["gpt-5.5*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Listed on the official GPT-5.5 model page. gpt-5.5-pro is responses-only (see separate entry above)."},{"family":"gpt-5.4","patterns":["gpt-5.4*"],"routingPatterns":["gpt-5.4","gpt-5.4-2026-03-05"],"endpoints":["chat_completions","responses"],"routing":{"reasoningEfforts":["none","low","medium","high","xhigh"],"contextWindowTokens":1050000},"notes":"Supports both endpoints on OpenAI. GPT-5.4 routing metadata is exact-model only; mini/nano variants need separately verified data. The model page lists none, low, medium, high, and xhigh reasoning effort and a 1,050,000-token context window."},{"family":"gpt-5.3-codex","patterns":["gpt-5.3-codex","gpt-5.3-codex-*"],"endpoints":["responses"],"notes":"Responses API only per official model page (developers.openai.com/api/docs/models/gpt-5.3-codex). More specific than gpt-5.3* pattern below."},{"family":"gpt-5.3","patterns":["gpt-5.3*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Only gpt-5.3-chat-latest confirmed for both endpoints; gpt-5.3-codex is responses-only (see separate entry above). No bare gpt-5.3 base model exists."},{"family":"gpt-5.2-codex","patterns":["gpt-5.2-codex","gpt-5.2-codex-*"],"endpoints":["responses"],"notes":"Responses API only per official model page (developers.openai.com/api/docs/models/gpt-5.2-codex). More specific than gpt-5.2* pattern below."},{"family":"gpt-5.2-pro","patterns":["gpt-5.2-pro","gpt-5.2-pro-*"],"endpoints":["responses"],"notes":"Responses API only per official model page (developers.openai.com/api/docs/models/gpt-5.2-pro). More specific than gpt-5.2* pattern below."},{"family":"gpt-5.2","patterns":["gpt-5.2*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. gpt-5.2-pro and gpt-5.2-codex are responses-only (see separate entries above)."},{"family":"gpt-5.1-codex-max","patterns":["gpt-5.1-codex-max"],"endpoints":["responses"],"notes":"Responses API only (ResponsesOnlyModel)."},{"family":"gpt-5.1-codex","patterns":["gpt-5.1-codex","gpt-5.1-codex-*"],"endpoints":["responses"],"notes":"Responses API only per official model pages (developers.openai.com/api/docs/models/gpt-5.1-codex, /gpt-5.1-codex-mini). More specific than gpt-5.1* pattern below. gpt-5.1-codex-max is covered by a dedicated entry above."},{"family":"gpt-5.1","patterns":["gpt-5.1*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints for gpt-5.1 and gpt-5.1-mini. gpt-5.1-codex, gpt-5.1-codex-mini, and gpt-5.1-codex-max are responses-only (see separate entries above)."},{"family":"gpt-5-codex/pro","patterns":["gpt-5-codex","gpt-5-pro","gpt-5-pro-*"],"endpoints":["responses"],"notes":"Responses API only for gpt-5-codex and gpt-5-pro (ResponsesOnlyModel). Includes dated variants like gpt-5-pro-2025-10-06."},{"family":"gpt-5","patterns":["gpt-5","gpt-5-*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints for gpt-5, gpt-5-mini, gpt-5-nano."},{"family":"o4-mini-deep-research","patterns":["o4-mini-deep-research","o4-mini-deep-research-*"],"endpoints":["responses"],"notes":"Responses API only (ResponsesOnlyModel). Includes dated variants like o4-mini-deep-research-2025-06-26."},{"family":"o4","patterns":["o4*"],"endpoints":["chat_completions","responses"],"notes":"Reasoning model. o4-mini supports both endpoints (ModelIdsShared)."},{"family":"o3-pro/deep-research","patterns":["o3-pro","o3-pro-*","o3-deep-research","o3-deep-research-*"],"endpoints":["responses"],"notes":"Responses API only. o3-pro and o3-deep-research are in ResponsesModel but not ChatModel."},{"family":"o3","patterns":["o3*"],"endpoints":["chat_completions","responses"],"notes":"Reasoning model. o3, o3-mini support both endpoints. o3-pro/deep-research use responses only (covered by separate entry above)."},{"family":"o1-pro","patterns":["o1-pro","o1-pro-*"],"endpoints":["responses"],"notes":"Responses API only. o1-pro is in ResponsesModel but not ChatModel."},{"family":"o1-mini/preview","patterns":["o1-mini*","o1-preview*"],"endpoints":["chat_completions"],"notes":"Chat Completions API only per official model pages (developers.openai.com/api/docs/models/o1-mini, /o1-preview). Not available via Responses API. More specific than o1* pattern below."},{"family":"o1","patterns":["o1*"],"endpoints":["chat_completions","responses"],"notes":"Reasoning model. o1 (base) supports both endpoints. o1-pro is responses-only and o1-mini/o1-preview are chat_completions-only (see separate entries above)."},{"family":"computer-use-preview","patterns":["computer-use-preview*"],"endpoints":["responses"],"notes":"Responses API only. Specialized model for computer use tasks."},{"family":"codex-mini","patterns":["codex-mini*"],"endpoints":["responses"],"notes":"Responses API only per official model page (developers.openai.com/api/docs/models/codex-mini-latest). Chat Completions is marked unsupported on the model page."},{"family":"chatgpt-4o-latest","patterns":["chatgpt-4o-latest"],"endpoints":["chat_completions","responses"],"notes":"Rolling alias for the latest ChatGPT-optimized GPT-4o model. Supports both endpoints (ModelIdsShared). Not matched by gpt-4o* pattern due to chatgpt- prefix."},{"family":"gpt-4o","patterns":["gpt-4o*"],"endpoints":["chat_completions","responses"],"notes":"Multimodal. Supports both endpoints. Includes audio and search preview variants."},{"family":"gpt-4.1","patterns":["gpt-4.1*"],"endpoints":["chat_completions","responses"],"notes":"Supports both endpoints. Includes gpt-4.1-mini, gpt-4.1-nano."},{"family":"gpt-4.5","patterns":["gpt-4.5*"],"endpoints":["chat_completions","responses"],"notes":"Preview model. Now supports both endpoints per official model page (developers.openai.com/api/docs/models/gpt-4.5-preview); Responses API support was added since the previous mapping update."},{"family":"gpt-4-turbo","patterns":["gpt-4-turbo*"],"endpoints":["chat_completions","responses"],"notes":"Legacy. In ModelIdsShared per OpenAI spec v2.3.0; supports both endpoints."},{"family":"gpt-4","patterns":["gpt-4","gpt-4-*"],"endpoints":["chat_completions","responses"],"notes":"Legacy. In ModelIdsShared per OpenAI spec v2.3.0; supports both endpoints."},{"family":"gpt-3.5-turbo","patterns":["gpt-3.5-turbo*"],"endpoints":["chat_completions","responses"],"notes":"Legacy. In ModelIdsShared per OpenAI spec v2.3.0; supports both endpoints. Deprecated."}]},"anthropic":{"endpoints":{"messages":{"path":"/v1/messages","description":"Unified Messages API for all Claude models. Supports tool use, streaming, extended thinking, and vision."}},"models":[{"family":"claude-opus-5","patterns":["claude-opus-5*"],"routingPatterns":["claude-opus-5-5","claude-opus-5"],"endpoints":["messages"],"routing":{"reasoningEfforts":["low","medium","high","max"],"contextWindowTokens":1000000},"notes":"Flagship Opus generation 5 models with verified 1M-token context windows and the Anthropic effort parameter."},{"family":"claude-fable-5","patterns":["claude-fable-5*"],"routingPatterns":["claude-fable-5-1","claude-fable-5"],"endpoints":["messages"],"routing":{"reasoningEfforts":["low","medium","high","max"],"contextWindowTokens":1000000},"notes":"Latest flagship family with verified 1M-token context windows and the Anthropic effort parameter."},{"family":"claude-mythos-5","patterns":["claude-mythos-5*","claude-mythos-preview*"],"routingPatterns":["claude-mythos-5-1","claude-mythos-5","claude-mythos-preview*"],"endpoints":["messages"],"routing":{"reasoningEfforts":["low","medium","high","max"],"contextWindowTokens":1000000},"notes":"Specialized models, invitation-only, with verified 1M-token context windows and the Anthropic effort parameter."},{"family":"claude-sonnet-5","patterns":["claude-sonnet-5*"],"routingPatterns":["claude-sonnet-5-5","claude-sonnet-5"],"endpoints":["messages"],"routing":{"reasoningEfforts":["low","medium","high","max"],"contextWindowTokens":1000000},"notes":"Best combination of speed and intelligence with verified 1M-token context windows and the Anthropic effort parameter."},{"family":"claude-opus-4","patterns":["claude-opus-4*"],"routingPatterns":["claude-opus-4-8","claude-opus-4-7","claude-opus-4-6"],"endpoints":["messages"],"routing":{"reasoningEfforts":["low","medium","high","max"],"contextWindowTokens":1000000},"notes":"Opus 4.6 and newer models have verified 1M-token context windows and support the Anthropic effort parameter."},{"family":"claude-sonnet-4","patterns":["claude-sonnet-4*"],"routingPatterns":["claude-sonnet-4-6"],"endpoints":["messages"],"routing":{"reasoningEfforts":["low","medium","high","max"],"contextWindowTokens":1000000},"notes":"Sonnet 4.6 has a verified 1M-token context window and supports the Anthropic effort parameter."},{"family":"claude-haiku-5","patterns":["claude-haiku-5*"],"endpoints":["messages"],"notes":"Fast/cheap Haiku generation 5 (claude-haiku-5-5). 1M token context window per official models overview; effort levels not verified, so no routing metadata."},{"family":"claude-haiku-4","patterns":["claude-haiku-4*"],"endpoints":["messages"],"notes":"Includes claude-haiku-4-5. Fast/cheap. 200K token context."},{"family":"claude-3.5-sonnet","patterns":["claude-3-5-sonnet*","claude-3.5-sonnet*"],"endpoints":["messages"],"notes":"Previous generation. 200K token context."},{"family":"claude-3.5-haiku","patterns":["claude-3-5-haiku*","claude-3.5-haiku*"],"endpoints":["messages"],"notes":"Previous generation fast model."},{"family":"claude-3.7-sonnet","patterns":["claude-3-7-sonnet*","claude-3.7-sonnet*"],"endpoints":["messages"],"notes":"Previous generation extended thinking model. 200K token context."},{"family":"claude-3-opus","patterns":["claude-3-opus*"],"endpoints":["messages"],"notes":"Previous generation. 200K token context."}]}},"error":null},"routing":null}
\ No newline at end of file
diff --git a/actions/setup/js/types/unified_session.d.ts b/actions/setup/js/types/unified_session.d.ts
index 99c0e679019..11a201e85d7 100644
--- a/actions/setup/js/types/unified_session.d.ts
+++ b/actions/setup/js/types/unified_session.d.ts
@@ -228,10 +228,18 @@ export interface ModelRoutingOutcomeData {
export interface ModelInventoryData {
endpoints: {
provider: string;
- models: string[];
+ /** AWF's endpoint configuration state; absent when the source did not supply it. */
+ configured?: boolean;
+ /** Advertised model IDs in source order; absent means unknown, not empty. */
+ models?: string[];
+ /** Routing metadata in source order; absent means unknown, not empty. */
routingModels?: {
modelId: string;
supportedEndpoints?: string[];
+ /** Identifier-shaped reasoning efforts; absent means unknown; a supplied empty array stays empty. */
+ supportedReasoningEfforts?: string[];
+ /** Positive context window size in tokens; absent means unknown. */
+ contextWindowTokens?: number;
}[];
}[];
modelsFetchComplete?: boolean;
diff --git a/actions/setup/js/unified_session.test.cjs b/actions/setup/js/unified_session.test.cjs
index 7da946726a1..e725f459a14 100644
--- a/actions/setup/js/unified_session.test.cjs
+++ b/actions/setup/js/unified_session.test.cjs
@@ -186,6 +186,8 @@ describe("Unified conclusion session", () => {
'{"providers":{"copilot":{"models":["a",false]}}}',
'{"provider":"copilot","models":["a",42]}',
'{"endpoints":[{"provider":"copilot","models":[],"routing_models":[{"model_id":42}]}]}',
+ '{"endpoints":[{"provider":"copilot","configured":true,"models":["a"],"routing_models":[{"model_id":"a","supported_reasoning_efforts":["very high"]}]}]}',
+ '{"endpoints":[{"provider":"copilot","configured":true,"models":["a"],"routing_models":[{"model_id":"a","context_window_tokens":0}]}]}',
])("warns once and skips malformed model inventories: %s", content => {
write("sandbox/firewall/models.json", content);
const warn = vi.fn();
@@ -232,7 +234,7 @@ describe("Unified conclusion session", () => {
],
[{ providers: {} }, { endpoints: [] }],
[{ provider: "copilot", models: [] }, { endpoints: [{ provider: "copilot", models: [] }] }],
- [{ endpoints: [{ provider: "copilot", configured: false, models: null }] }, { endpoints: [{ provider: "copilot", models: [] }] }],
+ [{ endpoints: [{ provider: "copilot", configured: false, models: null }] }, { endpoints: [{ provider: "copilot", configured: false }] }],
])("keeps genuine empty or unconfigured preferred inventories instead of stale mirrors: %j", (inventory, expected) => {
write("agent/awf-reflect.json", inventory);
write("sandbox/firewall/awf-reflect.json", { endpoints: [{ provider: "stale", models: ["stale"] }] });
diff --git a/actions/setup/js/unified_session_payload.cjs b/actions/setup/js/unified_session_payload.cjs
index ee206e6a0b1..a09a693a803 100644
--- a/actions/setup/js/unified_session_payload.cjs
+++ b/actions/setup/js/unified_session_payload.cjs
@@ -472,12 +472,17 @@ function resolveUsageAic(source, data, usage) {
/**
* Validate and project inventory formats together so collectors cannot accept
* malformed snapshots that would otherwise look like genuinely empty inventories.
+ * AWF writes null model and routing collections for unconfigured endpoints and
+ * pending fetches; those stay unknown (omitted), never become empty arrays.
* @param {any} value
* @returns {import("./types/unified_session").ModelInventoryData | undefined}
*/
function normalizeModelInventoryData(value) {
const isObject = entry => entry !== null && typeof entry === "object" && !Array.isArray(entry);
- const isIdentifier = entry => typeof entry === "string" && entry.trim().length > 0;
+ // Model IDs, providers, and endpoints only need to be nonblank strings.
+ const isNonBlank = entry => typeof entry === "string" && entry.trim().length > 0;
+ // Reasoning efforts are short identifier tokens such as "low" or "xhigh", never free-form text.
+ const isEffortToken = entry => typeof entry === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(entry);
const firstField = (entry, names) => names.map(name => entry[name]).find(field => field !== undefined);
const normalizeRouting = entries => {
if (!Array.isArray(entries)) return undefined;
@@ -486,8 +491,17 @@ function normalizeModelInventoryData(value) {
if (!isObject(entry)) return undefined;
const modelId = firstField(entry, ["modelId", "model_id"]);
const supportedEndpoints = firstField(entry, ["supportedEndpoints", "supported_endpoints"]);
- if (!isIdentifier(modelId) || (supportedEndpoints !== undefined && (!Array.isArray(supportedEndpoints) || !supportedEndpoints.every(isIdentifier)))) return undefined;
- result.push({ modelId, ...(supportedEndpoints !== undefined ? { supportedEndpoints: [...supportedEndpoints] } : {}) });
+ const efforts = firstField(entry, ["supportedReasoningEfforts", "supported_reasoning_efforts"]);
+ const contextWindow = firstField(entry, ["contextWindowTokens", "context_window_tokens"]);
+ if (!isNonBlank(modelId) || (supportedEndpoints !== undefined && (!Array.isArray(supportedEndpoints) || !supportedEndpoints.every(isNonBlank)))) return undefined;
+ if (efforts != null && (!Array.isArray(efforts) || !efforts.every(isEffortToken))) return undefined;
+ if (contextWindow != null && (!Number.isSafeInteger(contextWindow) || contextWindow <= 0)) return undefined;
+ result.push({
+ modelId,
+ ...(supportedEndpoints !== undefined ? { supportedEndpoints: [...supportedEndpoints] } : {}),
+ ...(efforts != null ? { supportedReasoningEfforts: [...efforts] } : {}),
+ ...(contextWindow != null ? { contextWindowTokens: contextWindow } : {}),
+ });
}
return result;
};
@@ -509,26 +523,39 @@ function normalizeModelInventoryData(value) {
return undefined;
}
const topRouting = firstField(value, ["routingModels", "routing_models"]);
- const topRoutingModels = topRouting === undefined ? undefined : normalizeRouting(topRouting);
- if (topRouting !== undefined && topRoutingModels === undefined) return undefined;
+ const topRoutingModels = topRouting == null ? undefined : normalizeRouting(topRouting);
+ if (topRouting != null && topRoutingModels === undefined) return undefined;
/** @type {import("./types/unified_session").ModelInventoryData["endpoints"]} */
const endpoints = [];
for (const entry of entries) {
- if (!isObject(entry) || !isIdentifier(entry.provider)) return undefined;
+ if (!isObject(entry) || !isNonBlank(entry.provider)) return undefined;
+ const configured = entry.configured;
+ if (configured !== undefined && typeof configured !== "boolean") return undefined;
const rawModels = firstField(entry, ["models", "available_models", "detected_models", "model_ids", "availableModels"]);
- // AWF uses null for unconfigured endpoints and pending model fetches.
- const models = rawModels === null && typeof entry.configured === "boolean" ? [] : rawModels;
- if (!Array.isArray(models)) return undefined;
- const modelIds = [];
- for (const model of models) {
- const id = typeof model === "string" ? model : isObject(model) ? firstField(model, ["id", "name", "model"]) : undefined;
- if (!isIdentifier(id)) return undefined;
- modelIds.push(id);
+ /** @type {string[] | undefined} */
+ let modelIds;
+ if (rawModels == null) {
+ // Only AWF's explicit endpoint state makes a missing model list an unknown rather than malformed inventory.
+ if (configured === undefined) return undefined;
+ } else {
+ if (!Array.isArray(rawModels)) return undefined;
+ modelIds = [];
+ for (const model of rawModels) {
+ const id = typeof model === "string" ? model : isObject(model) ? firstField(model, ["id", "name", "model"]) : undefined;
+ if (!isNonBlank(id)) return undefined;
+ modelIds.push(id);
+ }
}
const endpointRouting = firstField(entry, ["routingModels", "routing_models"]);
- const routingModels = endpointRouting === undefined ? topRoutingModels : normalizeRouting(endpointRouting);
- if (endpointRouting !== undefined && routingModels === undefined) return undefined;
- endpoints.push({ provider: entry.provider, models: modelIds, ...(routingModels !== undefined ? { routingModels: structuredClone(routingModels) } : {}) });
+ // An explicit endpoint-local null means no routing metadata; only an absent field inherits the top-level list.
+ const routingModels = endpointRouting === undefined ? topRoutingModels : endpointRouting === null ? undefined : normalizeRouting(endpointRouting);
+ if (endpointRouting != null && routingModels === undefined) return undefined;
+ endpoints.push({
+ provider: entry.provider,
+ ...(configured !== undefined ? { configured } : {}),
+ ...(modelIds !== undefined ? { models: modelIds } : {}),
+ ...(routingModels !== undefined ? { routingModels: structuredClone(routingModels) } : {}),
+ });
}
const modelsFetchComplete = firstField(value, ["modelsFetchComplete", "models_fetch_complete"]);
if (modelsFetchComplete !== undefined && typeof modelsFetchComplete !== "boolean") return undefined;
diff --git a/actions/setup/js/unified_session_payload.test.cjs b/actions/setup/js/unified_session_payload.test.cjs
index df14c8f961a..057951ce7cf 100644
--- a/actions/setup/js/unified_session_payload.test.cjs
+++ b/actions/setup/js/unified_session_payload.test.cjs
@@ -1,8 +1,11 @@
-import { describe, expect, it } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import crypto from "node:crypto";
import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
import { fileURLToPath } from "node:url";
import { normalizeUnifiedSessionEvent, normalizeModelInventoryData } from "./unified_session_payload.cjs";
-import { mergeSessionSources } from "./unified_session.cjs";
+import { collectUnifiedSession, mergeSessionSources, writeUnifiedSession } from "./unified_session.cjs";
import { generatePlainTextSummary, generateCopilotCliStyleSummary, convertCopilotEventsToLegacyLogEntries } from "./log_parser_shared.cjs";
import { serializeSessionArtifact } from "./session_artifact.cjs";
import { normalizeCodexSession } from "./codex_session.cjs";
@@ -11,7 +14,7 @@ import { normalizeCopilotSession } from "./copilot_session.cjs";
import { normalizeGeminiSession } from "./gemini_session.cjs";
import { collapseStreamedMessages } from "./agent_session_render.cjs";
import { reconcileSessionUsage, selectSessionResult, sessionTokenTotal } from "./agent_session.cjs";
-import { createSessionValidator } from "./scripts/validate_session.cjs";
+import { createSessionValidator, validateSession } from "./scripts/validate_session.cjs";
import { dynamicWorkflow } from "./fixtures/claude_dynamic_workflow.cjs";
import { DYNAMIC_WORKFLOW_EVENT_TYPES } from "./dynamic_workflow_session.cjs";
@@ -97,13 +100,21 @@ describe("essential unified session payloads", () => {
[{ endpoints: [] }, { endpoints: [] }],
[{ providers: {} }, { endpoints: [] }],
[{ provider: "copilot", models: [] }, { endpoints: [{ provider: "copilot", models: [] }] }],
- [{ endpoints: [{ provider: "anthropic", configured: false, models: null }] }, { endpoints: [{ provider: "anthropic", models: [] }] }],
- [{ endpoints: [{ provider: "opencode", configured: true, models: null, models_url: null }] }, { endpoints: [{ provider: "opencode", models: [] }] }],
- ])("accepts genuine empty inventories and AWF null model collections: %j", (source, expected) => {
+ [{ endpoints: [{ provider: "anthropic", configured: false, models: null }] }, { endpoints: [{ provider: "anthropic", configured: false }] }],
+ [{ endpoints: [{ provider: "opencode", configured: true, models: null, models_url: null }] }, { endpoints: [{ provider: "opencode", configured: true }] }],
+ [{ endpoints: [{ provider: "copilot", configured: true, models: ["a"], routing_models: null }] }, { endpoints: [{ provider: "copilot", configured: true, models: ["a"] }] }],
+ [{ routing_models: [{ model_id: "fallback" }], endpoints: [{ provider: "copilot", configured: false, models: null, routing_models: null }] }, { endpoints: [{ provider: "copilot", configured: false }] }],
+ [{ routing_models: null, endpoints: [{ provider: "copilot", models: [] }] }, { endpoints: [{ provider: "copilot", models: [] }] }],
+ [
+ { endpoints: [{ provider: "copilot", models: ["a"], routing_models: [{ model_id: "a", supported_reasoning_efforts: [], context_window_tokens: null }] }] },
+ { endpoints: [{ provider: "copilot", models: ["a"], routingModels: [{ modelId: "a", supportedReasoningEfforts: [] }] }] },
+ ],
+ ])("accepts genuine empty inventories and keeps AWF null collections unknown: %j", (source, expected) => {
expect(normalizeModelInventoryData(source)).toEqual(expected);
const event = normalizeUnifiedSessionEvent({ type: "model.inventory", data: source });
expect(event.data).toEqual(expected);
expect(normalizeUnifiedSessionEvent(event)).toEqual(event);
+ expect(normalizeModelInventoryData(JSON.parse(JSON.stringify(event.data)))).toEqual(expected);
});
it.each([
@@ -137,6 +148,17 @@ describe("essential unified session payloads", () => {
{ endpoints: [], routing_models: [{ model_id: "model", supported_endpoints: ["/responses", 42] }] },
{ provider: "copilot", models: [], routingModels: [null] },
{ provider: "copilot", models: [], routingModels: [{ modelId: "model", supportedEndpoints: null }] },
+ { endpoints: [{ provider: "copilot", configured: "true", models: ["model"] }] },
+ { endpoints: [{ provider: "copilot", configured: true, models: null, routing_models: "malformed" }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", supportedReasoningEfforts: "high" }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", supportedReasoningEfforts: ["high", 42] }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", supportedReasoningEfforts: ["very high"] }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", supportedReasoningEfforts: [""] }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", contextWindowTokens: 0 }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", contextWindowTokens: -1 }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", contextWindowTokens: 1.5 }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", contextWindowTokens: "128000" }] },
+ { provider: "copilot", models: [], routingModels: [{ modelId: "model", contextWindowTokens: Number.MAX_SAFE_INTEGER + 1 }] },
{ endpoints: [], models_fetch_complete: "true" },
])("rejects malformed inventory rather than producing a success-shaped empty payload: %j", source => {
expect(normalizeModelInventoryData(source)).toBeUndefined();
@@ -1215,3 +1237,249 @@ describe("AWF model routing candidate and request metadata", () => {
}
});
});
+
+describe("AWF model inventories with unknown endpoints and live reasoning metadata", () => {
+ const FIXTURE_DIR = fileURLToPath(new URL("./test_data/awf_reflect/", import.meta.url));
+ const GOLDEN_DIR = fileURLToPath(new URL("../../../pkg/cli/testdata/model_routing_golden/", import.meta.url));
+ const REFLECT_PATH = "sandbox/firewall/awf-reflect.json";
+ const INVENTORY_SOURCES = ["agent/awf-reflect.json", REFLECT_PATH, "sandbox/firewall/models.json"];
+ // Recorded fixtures; see test_data/awf_reflect/README.md for run, artifact, and digest provenance.
+ const RECORDED = { file: "run-38995410124-awf-reflect.json", sha256: "9d59511015584d359b4159737d1d44ed12ad9426a1690e62148ab53b7543648a" };
+ const LEGACY = { file: "run-29812420778-legacy-awf-reflect.json", sha256: "da2570969e0d62bd10195f029b5472f750404b515784d8c68e9b267e0975b410" };
+ const ENDPOINT_KEYS = ["provider", "configured", "models", "routingModels"];
+ const ROUTING_KEYS = ["modelId", "supportedEndpoints", "supportedReasoningEfforts", "contextWindowTokens"];
+ const sha256 = bytes => crypto.createHash("sha256").update(bytes).digest("hex");
+ const readFixture = ({ file, sha256: digest }) => {
+ const bytes = fs.readFileSync(path.join(FIXTURE_DIR, file));
+ expect(sha256(bytes)).toBe(digest);
+ return bytes;
+ };
+ const classify = endpoint => (!Object.hasOwn(endpoint, "models") ? "unknown" : endpoint.models.length ? "listed" : "empty");
+ const roots = [];
+ afterEach(() => {
+ for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
+ });
+
+ // Collect and publish from a root containing only the stated inventory source.
+ function publish(files) {
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-model-inventory-"));
+ roots.push(root);
+ for (const [file, content] of Object.entries(files)) {
+ fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
+ fs.writeFileSync(path.join(root, file), content);
+ }
+ const warn = vi.fn();
+ writeUnifiedSession({ rootDir: root, warn, dailyAIC: {}, jobResults: "" });
+ const content = fs.readFileSync(path.join(root, "usage/aw_session.jsonl"), "utf8");
+ expect(validateSession(content)).toBeGreaterThan(0);
+ const events = content
+ .trim()
+ .split("\n")
+ .map(line => JSON.parse(line));
+ return {
+ events,
+ warn,
+ inventories: events.filter(event => event.type === "model.inventory"),
+ inventoryWarnings: events.filter(event => event.type === "session.collection_warning" && event.data.code === "invalid_model_inventory"),
+ };
+ }
+
+ // Canonical projection is deterministic, idempotent, and survives JSON round trips.
+ function expectStableProjection(data) {
+ expect(normalizeModelInventoryData(data)).toEqual(data);
+ expect(normalizeModelInventoryData(JSON.parse(JSON.stringify(data)))).toEqual(data);
+ const event = normalizeUnifiedSessionEvent({ type: "model.inventory", data });
+ expect(event.data).toEqual(data);
+ expect(normalizeUnifiedSessionEvent(structuredClone(event))).toEqual(event);
+ expect(JSON.stringify(normalizeModelInventoryData(data))).toBe(JSON.stringify(data));
+ }
+
+ function expectAllowlisted(data) {
+ for (const endpoint of data.endpoints) {
+ expect(Object.keys(endpoint).every(key => ENDPOINT_KEYS.includes(key))).toBe(true);
+ for (const routing of endpoint.routingModels ?? []) expect(Object.keys(routing).every(key => ROUTING_KEYS.includes(key))).toBe(true);
+ }
+ const serialized = JSON.stringify(data);
+ for (const value of ["http", "api-proxy", "base_url", "models_url", "credential", "Bearer", "github_token", "pricing", "candidate_metadata_complete", '"source"']) expect(serialized).not.toContain(value);
+ }
+
+ it("preserves the complete recorded 38995410124 inventory with unknown unconfigured endpoints", () => {
+ const bytes = readFixture(RECORDED);
+ const source = JSON.parse(bytes.toString("utf8"));
+ const original = structuredClone(source);
+ const { inventories, inventoryWarnings, warn } = publish({ [REFLECT_PATH]: bytes });
+ expect(inventories).toHaveLength(1);
+ expect(inventoryWarnings).toHaveLength(0);
+ expect(warn).not.toHaveBeenCalledWith(expect.stringContaining("invalid_model_inventory"));
+ const [inventory] = inventories;
+ expect(inventory.provenance).toEqual({ component: "firewall", phase: "agent", path: REFLECT_PATH, index: 0 });
+ const { data } = inventory;
+ expect(data.modelsFetchComplete).toBe(true);
+ expect(data.endpoints.map(endpoint => endpoint.provider)).toEqual(source.endpoints.map(endpoint => endpoint.provider));
+ expect(data.endpoints).toHaveLength(5);
+ for (const [index, endpoint] of data.endpoints.entries()) {
+ const recorded = source.endpoints[index];
+ if (endpoint.provider === "copilot") continue;
+ expect(["openai", "anthropic", "gemini", "vertex"]).toContain(endpoint.provider);
+ expect(recorded.models).toBeNull();
+ expect(recorded.routing_models).toBeNull();
+ expect(endpoint).toEqual({ provider: recorded.provider, configured: false });
+ expect(Object.hasOwn(endpoint, "models")).toBe(false);
+ expect(Object.hasOwn(endpoint, "routingModels")).toBe(false);
+ }
+ const copilot = data.endpoints.find(endpoint => endpoint.provider === "copilot");
+ const recordedCopilot = source.endpoints.find(endpoint => endpoint.provider === "copilot");
+ expect(copilot.configured).toBe(true);
+ expect(copilot.models).toHaveLength(40);
+ expect(copilot.models).toEqual(recordedCopilot.models);
+ expect(copilot.routingModels).toHaveLength(40);
+ expect(copilot.routingModels.map(entry => entry.modelId)).toEqual(recordedCopilot.routing_models.map(entry => entry.model_id));
+ expect(copilot.routingModels.map(entry => entry.supportedEndpoints)).toEqual(recordedCopilot.routing_models.map(entry => entry.supported_endpoints));
+ expect(copilot.routingModels.map(entry => entry.supportedReasoningEfforts)).toEqual(recordedCopilot.routing_models.map(entry => entry.supported_reasoning_efforts));
+ expect(copilot.routingModels.some(entry => entry.supportedReasoningEfforts.length === 0)).toBe(true);
+ const known = recordedCopilot.routing_models.filter(entry => entry.context_window_tokens !== null);
+ expect(known).toHaveLength(36);
+ for (const [index, entry] of copilot.routingModels.entries()) {
+ const recordedWindow = recordedCopilot.routing_models[index].context_window_tokens;
+ if (recordedWindow === null) expect(Object.hasOwn(entry, "contextWindowTokens")).toBe(false);
+ else expect(entry.contextWindowTokens).toBe(recordedWindow);
+ }
+ expect(copilot.routingModels.filter(entry => !Object.hasOwn(entry, "contextWindowTokens"))).toHaveLength(4);
+ expectAllowlisted(data);
+ expectStableProjection(data);
+ expect(normalizeModelInventoryData(source)).toEqual(data);
+ expect(source).toEqual(original);
+ expect(sha256(fs.readFileSync(path.join(FIXTURE_DIR, RECORDED.file)))).toBe(RECORDED.sha256);
+ });
+
+ it("keeps a configured pending endpoint unknown instead of inventing an empty model list", () => {
+ // Pending-fetch variant of the recorded reflection: the recorded Copilot endpoint with null collections, fetch incomplete.
+ const recorded = JSON.parse(readFixture(RECORDED).toString("utf8"));
+ const recordedCopilot = recorded.endpoints.find(endpoint => endpoint.provider === "copilot");
+ const pending = { ...recorded, models_fetch_complete: false, endpoints: [{ ...recordedCopilot, models: null, routing_models: null }] };
+ const original = structuredClone(pending);
+ const { inventories, inventoryWarnings } = publish({ [REFLECT_PATH]: JSON.stringify(pending) });
+ expect(inventories).toHaveLength(1);
+ expect(inventoryWarnings).toHaveLength(0);
+ const { data } = inventories[0];
+ expect(data).toEqual({ endpoints: [{ provider: "copilot", configured: true }], modelsFetchComplete: false });
+ const [endpoint] = data.endpoints;
+ expect(Object.hasOwn(endpoint, "models")).toBe(false);
+ expect(Object.hasOwn(endpoint, "routingModels")).toBe(false);
+ expect(classify(endpoint)).toBe("unknown");
+ const roundTrip = JSON.parse(JSON.stringify(normalizeModelInventoryData(data)));
+ expect(roundTrip.endpoints[0]).toEqual({ provider: "copilot", configured: true });
+ expect(classify(roundTrip.endpoints[0])).toBe("unknown");
+ expectStableProjection(data);
+ expect(pending).toEqual(original);
+ });
+
+ it("preserves the existing projection of a recorded legacy golden reflection", () => {
+ // Recorded legacy reflection from run 29812420778, which predates AWF routing metadata.
+ // No golden case retains its source reflection; see test_data/awf_reflect/README.md.
+ const bytes = readFixture(LEGACY);
+ const source = JSON.parse(bytes.toString("utf8"));
+ const original = structuredClone(source);
+ expect(source.endpoints.some(endpoint => Object.hasOwn(endpoint, "routing_models"))).toBe(false);
+ expect(Object.hasOwn(source, "routing_models")).toBe(false);
+ // Output of the pre-change normalizer on this file: AWF null model lists became empty arrays and `configured` was dropped.
+ const preChange = { endpoints: source.endpoints.map(endpoint => ({ provider: endpoint.provider, models: endpoint.models ?? [] })), modelsFetchComplete: true };
+ const { inventories, inventoryWarnings } = publish({ [REFLECT_PATH]: bytes });
+ expect(inventories).toHaveLength(1);
+ expect(inventoryWarnings).toHaveLength(0);
+ const { data } = inventories[0];
+ expect(data.endpoints.map(endpoint => endpoint.provider)).toEqual(preChange.endpoints.map(endpoint => endpoint.provider));
+ expect(data.modelsFetchComplete).toBe(preChange.modelsFetchComplete);
+ for (const [index, endpoint] of data.endpoints.entries()) {
+ const recorded = source.endpoints[index];
+ expect(endpoint.configured).toBe(recorded.configured);
+ if (recorded.models === null) {
+ // Only correction: an unconfigured endpoint's unknown list is no longer reported as empty.
+ expect(endpoint).toEqual({ provider: recorded.provider, configured: recorded.configured });
+ expect(preChange.endpoints[index]).toEqual({ provider: recorded.provider, models: [] });
+ } else {
+ expect(endpoint).toEqual({ ...preChange.endpoints[index], configured: recorded.configured });
+ expect(endpoint.models).toHaveLength(36);
+ }
+ expect(Object.hasOwn(endpoint, "routingModels")).toBe(false);
+ }
+ expect(JSON.stringify(data)).not.toMatch(/supportedReasoningEfforts|contextWindowTokens|routingModels/);
+ expectAllowlisted(data);
+ expectStableProjection(data);
+ expect(source).toEqual(original);
+ });
+
+ it("warns and emits no inventory for a recorded reflection with a non-string model ID", () => {
+ // Malformed-input variant of the recorded reflection: only the first Copilot model ID is numeric.
+ const malformed = JSON.parse(readFixture(RECORDED).toString("utf8"));
+ const copilot = malformed.endpoints.find(endpoint => endpoint.provider === "copilot");
+ copilot.models[0] = 42;
+ expect(malformed.endpoints.filter(endpoint => endpoint.models === null)).toHaveLength(4);
+ expect(normalizeModelInventoryData(malformed)).toBeUndefined();
+ const { events, inventories, inventoryWarnings } = publish({ [REFLECT_PATH]: JSON.stringify(malformed) });
+ expect(inventories).toHaveLength(0);
+ expect(inventoryWarnings).toHaveLength(1);
+ expect(inventoryWarnings[0].data).toEqual({ path: REFLECT_PATH, code: "invalid_model_inventory" });
+ expect(JSON.stringify(events)).not.toContain('"endpoints"');
+ });
+
+ it.each([
+ ["non-array reasoning efforts", entry => (entry.supported_reasoning_efforts = "high")],
+ ["non-string reasoning effort", entry => (entry.supported_reasoning_efforts = ["high", 42])],
+ ["free-form reasoning effort", entry => (entry.supported_reasoning_efforts = ["high"])],
+ ["zero context window", entry => (entry.context_window_tokens = 0)],
+ ["fractional context window", entry => (entry.context_window_tokens = 1.5)],
+ ["string context window", entry => (entry.context_window_tokens = "200000")],
+ ["non-boolean configured", (_, endpoint) => (endpoint.configured = "true")],
+ ])("warns and emits no inventory for a recorded reflection variant with %s", (_, mutate) => {
+ // Malformed-input variant of the recorded reflection; the unconfigured endpoint nulls stay unchanged.
+ const malformed = JSON.parse(readFixture(RECORDED).toString("utf8"));
+ const copilot = malformed.endpoints.find(endpoint => endpoint.provider === "copilot");
+ mutate(copilot.routing_models[0], copilot);
+ expect(normalizeModelInventoryData(malformed)).toBeUndefined();
+ const { inventories, inventoryWarnings } = publish({ [REFLECT_PATH]: JSON.stringify(malformed) });
+ expect(inventories).toHaveLength(0);
+ expect(inventoryWarnings.map(event => event.data)).toEqual([{ path: REFLECT_PATH, code: "invalid_model_inventory" }]);
+ });
+
+ it("limits recorded model-routing golden changes to corrected inventory events", () => {
+ const cases = fs.readdirSync(GOLDEN_DIR).filter(name => fs.statSync(path.join(GOLDEN_DIR, name)).isDirectory());
+ expect(cases.length).toBeGreaterThan(0);
+ for (const name of cases) {
+ // Historical goldens never retained an inventory source, so none is synthesized for them.
+ for (const source of INVENTORY_SOURCES) expect(fs.existsSync(path.join(GOLDEN_DIR, name, source)), `${name} ${source}`).toBe(false);
+ const sessionFile = path.join(GOLDEN_DIR, name, "usage/aw_session.jsonl");
+ if (fs.existsSync(sessionFile)) {
+ const recorded = fs.readFileSync(sessionFile, "utf8");
+ expect(recorded, name).not.toContain('"model.inventory"');
+ expect(recorded, name).not.toContain("invalid_model_inventory");
+ }
+ const { events } = collectUnifiedSession({ rootDir: path.join(GOLDEN_DIR, name), warn: () => {}, dailyAIC: {}, jobResults: "" });
+ expect(
+ events.some(event => event.type === "model.inventory"),
+ name
+ ).toBe(false);
+ expect(
+ events.some(event => event.data?.code === "invalid_model_inventory"),
+ name
+ ).toBe(false);
+ }
+ // For the recorded inventory sources, the only changes a source makes are its single inventory event and its collection bookkeeping.
+ for (const fixture of [RECORDED, LEGACY]) {
+ const withSource = publish({ [REFLECT_PATH]: readFixture(fixture) });
+ const withoutSource = publish({});
+ expect(withSource.inventories).toHaveLength(1);
+ expect(withSource.inventoryWarnings).toHaveLength(0);
+ const unrelated = events => events.filter(event => event.type !== "model.inventory" && event.type !== "session.collection");
+ expect(unrelated(withSource.events)).toEqual(unrelated(withoutSource.events));
+ const collection = events => events.find(event => event.type === "session.collection").data;
+ const baseline = collection(withoutSource.events);
+ expect(collection(withSource.events)).toEqual({
+ ...baseline,
+ sources: [...baseline.sources, { component: "firewall", phase: "agent", path: REFLECT_PATH, events: 1 }],
+ untimedEvents: baseline.untimedEvents + 1,
+ absentComponents: baseline.absentComponents.filter(component => component !== "firewall"),
+ });
+ }
+ });
+});
diff --git a/docs/public/schemas/unified-session.schema.json b/docs/public/schemas/unified-session.schema.json
index 3e611aadf76..4bee481face 100644
--- a/docs/public/schemas/unified-session.schema.json
+++ b/docs/public/schemas/unified-session.schema.json
@@ -792,13 +792,25 @@
"type": "object",
"properties": {
"provider": { "type": "string" },
- "models": { "type": "array", "items": { "type": "string" } },
+ "configured": { "type": "boolean", "description": "AWF's endpoint configuration state; absent when the source did not supply it." },
+ "models": { "type": "array", "items": { "type": "string" }, "description": "Advertised model IDs in source order; absent means unknown, not empty." },
"routingModels": {
"type": "array",
- "items": { "type": "object", "properties": { "modelId": { "type": "string" }, "supportedEndpoints": { "type": "array", "items": { "type": "string" } } }, "required": ["modelId"], "additionalProperties": false }
+ "items": {
+ "type": "object",
+ "properties": {
+ "modelId": { "type": "string" },
+ "supportedEndpoints": { "type": "array", "items": { "type": "string" } },
+ "supportedReasoningEfforts": { "type": "array", "items": { "type": "string" }, "description": "Identifier-shaped reasoning efforts; absent means unknown; a supplied empty array stays empty." },
+ "contextWindowTokens": { "type": "number", "description": "Positive context window size in tokens; absent means unknown." }
+ },
+ "required": ["modelId"],
+ "additionalProperties": false
+ },
+ "description": "Routing metadata in source order; absent means unknown, not empty."
}
},
- "required": ["provider", "models"],
+ "required": ["provider"],
"additionalProperties": false
}
},
diff --git a/docs/src/content/docs/specs/unified-agent-session-specification.md b/docs/src/content/docs/specs/unified-agent-session-specification.md
index a393a721aee..0983adabd6d 100644
--- a/docs/src/content/docs/specs/unified-agent-session-specification.md
+++ b/docs/src/content/docs/specs/unified-agent-session-specification.md
@@ -3,7 +3,7 @@ title: Unified Agent Session Specification
description: Draft contract for canonical engine traces and essential unified session payloads across gh-aw runtime components.
sidebar:
order: 1365
-version: "1.12.0"
+version: "2.0.0"
status: Draft
publication_date: "2026-10-18"
editors:
@@ -13,7 +13,7 @@ editors:
# Unified Agent Session Specification
-**Version**: 1.12.0
+**Version**: 2.0.0
**Status**: Draft
**Publication Date**: 2026-10-18
**Editors**: GitHub Agentic Workflows Team (GitHub)
@@ -30,7 +30,7 @@ This specification defines the session traces used by GitHub Agentic Workflows:
This is a **GitHub Agentic Workflows project specification**, written using W3C-inspired document conventions. It is **not an official W3C standard**, W3C publication, or W3C-endorsed recommendation.
-Version 1.12.0 is a draft governed by the project's normal review process. It may be updated, replaced, or superseded. The accompanying implementation and regression suites exercise this contract, including the sampled CI sessions identified in Section 9.4. This is not a blanket declaration of conformance for every engine version or source format. Section 10 records historical pre-implementation gaps, not current defects. Approval and ongoing compliance testing remain project responsibilities.
+Version 2.0.0 is a draft governed by the project's normal review process. It may be updated, replaced, or superseded. The accompanying implementation and regression suites exercise this contract, including the sampled CI sessions identified in Section 9.4. This is not a blanket declaration of conformance for every engine version or source format. Section 10 records historical pre-implementation gaps, not current defects. Approval and ongoing compliance testing remain project responsibilities.
The specification version belongs to this document. The unified file's leading
`session.format` record carries an independent numeric serialization-format
@@ -641,7 +641,7 @@ source for opaque fields.
| `usage.report`, `execution.result`, `detection.result`, `workflow.info` | Existing accounting, execution evidence, detection verdicts, and run metadata. `workflow.info` retains available `cliVersion` (gh-aw), `awfVersion`, `mcpgVersion`, `engineId`, `agentVersion`, observed `model`, `requestedModel`, compact `modelRouting`, and `triggerType` from `aw_info.json`. `requestedModel` prefers `requested_model` and falls back to `model` for earlier metadata. Unavailable values are not inferred. |
| `workflow.job_result` | Conclusion's GitHub Actions dependency snapshot from `needs`, passed through `GH_AW_JOB_RESULTS`. Each record contains the stable `jobId` and its observed `result`: `success`, `failure`, `cancelled`, or `skipped`. Optional `durationMs` is the completed Actions job's `completed_at - started_at` in milliseconds, excluding queue time. Outputs and other dependency fields are excluded. Records have workflow component, conclusion phase, and `usage/aw_session.jsonl` provenance, with deterministic job-ID order and no invented timestamp. |
| `model_routing.outcome` | The harness's observed routing status, wire model, effective endpoint, selected endpoint, effort, applied effort, and failure code from `agent/awf-routing-outcome.json`; free-form detail is excluded. |
-| `model.inventory` | AWF's runtime model list from `sandbox/firewall/models.json` and persisted `/reflect` inventory, selecting the first valid copy from `agent/awf-reflect.json`, then `sandbox/firewall/awf-reflect.json`. Agent and threat-detection inventories retain their source path and phase separately. Payloads contain provider names, full model ID lists, routing model IDs and supported endpoints, and the observed `modelsFetchComplete` flag when supplied. Endpoint routing metadata takes precedence over the top-level fallback. Credentials, headers, URLs, and unrecognized fields are excluded. Missing inventory does not imply unavailable models; malformed sources emit collection warnings rather than empty inventory events. JSON parse failures produce one warning per source. |
+| `model.inventory` | AWF's runtime model list from `sandbox/firewall/models.json` and persisted `/reflect` inventory, selecting the first valid copy from `agent/awf-reflect.json`, then `sandbox/firewall/awf-reflect.json`. Agent and threat-detection inventories retain their source path and phase separately. Payloads contain provider names, AWF's endpoint `configured` state, full model ID lists, routing model IDs, supported endpoints, supported reasoning efforts, and context windows, and the observed `modelsFetchComplete` flag when supplied. Null or absent model and routing lists are unknown and omitted, never empty (T-UAS-075). Endpoint routing metadata takes precedence over the top-level fallback. Credentials, headers, URLs, and unrecognized fields are excluded. Missing inventory does not imply unavailable models; malformed sources emit collection warnings rather than empty inventory events. JSON parse failures produce one warning per source. |
| `model_endpoint.mismatch` | A single harness JSON record from `agent/model-endpoint-mismatch.json`, with the model/API mismatch cause and corrective action. |
| `model.resolution` | Runner-written records from `agent/model-resolution.json`, one per resolved role, with the engine, role, configured and resolved model, provider, status, and failure cause from the shared "resolve configured model" step (T-UAS-074). |
| `guardrail.daily_aic` | Activation's daily AI Credits decision (`status`, `exceeded`, and available numeric `total`, `estimated`, `threshold`). Omitted when the guardrail has no decision; an existing event is not duplicated. |
@@ -816,6 +816,49 @@ catalogue), `exact` (an exact id passed through unchanged), or `failed`; and
`catalog_unavailable` and `resolved_model` is `null`. Unknown source fields are
excluded; an absent record does not imply that a model was or was not resolved.
+#### Model inventory
+
+**T-UAS-075 — Model inventory endpoint state.** A `model.inventory` payload has
+an ordered `endpoints` array and an optional boolean `modelsFetchComplete`. Each
+endpoint has the following essential fields, in source order:
+
+- `provider`: required non-blank string.
+- `configured`: AWF's boolean endpoint state, `true` or `false`, when supplied.
+ The collector MUST NOT infer it from model counts or fetch completion, and MUST
+ leave it absent when the source omits it.
+- `models`: optional array of non-blank model ID strings in source order.
+- `routingModels`: optional array of entries in source order, each with a
+ required non-blank `modelId`, optional non-blank `supportedEndpoints` strings,
+ optional `supportedReasoningEfforts`, and optional `contextWindowTokens`.
+- `supportedReasoningEfforts`: identifier tokens of 1–64 characters that start
+ with a letter or digit and otherwise contain only letters, digits, `.`, `_`,
+ or `-` (for example `low` or `xhigh`).
+- `contextWindowTokens`: positive safe integer.
+
+AWF writes `null` model and routing lists for unconfigured endpoints and for
+configured endpoints whose model fetch is pending. The collector MUST omit such a
+property rather than write `null` or an empty array, and MUST still publish the
+other endpoints. Readers MUST treat an absent `models`, `routingModels`, or
+`supportedReasoningEfforts` as unknown, never as an empty list or as evidence that
+a model is unavailable. A supplied empty array MUST remain empty. An absent or
+`null` context window MUST be omitted, never written as zero or inferred from
+another model. A null model list is accepted only on an endpoint with a boolean
+`configured`. An explicit endpoint-local `null` routing list MUST NOT inherit the
+top-level routing list; only an absent endpoint-local list does. Snake_case
+source spellings (`routing_models`, `model_id`, `supported_endpoints`,
+`supported_reasoning_efforts`, `context_window_tokens`, and
+`models_fetch_complete`) normalize to these names.
+
+A source with a wrong collection type, a non-string or blank model ID, a
+non-boolean `configured`, an invalid reasoning effort, or a context window that
+is not a positive safe integer MUST produce one `invalid_model_inventory`
+collection warning and no inventory event for that source; the collector MUST NOT
+publish a partial or empty inventory in its place. Normalization MUST accept its
+own output unchanged, survive a JSON round trip, and leave the source unchanged.
+Each value comes from the recorded AWF source; credentials, headers, URLs,
+pricing, candidate-completeness flags, source labels, and other unrecognized
+endpoint or model fields MUST be excluded.
+
### 4.8 `agent.execution`
**T-UAS-067 — Unique execution diagnostics.** When an agent artifact contains an
@@ -1596,6 +1639,7 @@ Recommended execution is fixture parsing, canonical structural assertions, accou
| T-UAS-071 | Firewall token usage with supplied, absent, or malformed `steering`; AI-credit, token, and time steering event-log entries | Type and threshold survive projection, malformed notices are dropped, steering events keep threshold and request ID, and absent notices remain unrecorded rather than undelivered. |
| T-UAS-073 | Copilot built-in and declared sub-agents with distinct display names, provider-qualified SDK model IDs, declared concrete and alias models, and sessions without `agentName` | Rows keyed by `agentName` with sorted `display_names`, one audit row per definition and one logs row per definition and effort, bare served model IDs without aliases, unchanged counts and credits, and display-name rows when `agentName` is absent. |
| T-UAS-074 | `agent/model-resolution.json` with resolved, exact, and failed records for main and sub-agent roles; empty, non-array, malformed, and contract-violating records | One `model.resolution` event per record in array order with `model_resolution` provenance and only the contract fields; invalid sources emit collection warnings and no events. |
+| T-UAS-075 | Recorded AWF reflection from run 38995410124 with four unconfigured null endpoints; pending-fetch, legacy, and malformed-ID or capability-metadata variants | One inventory event per valid source with endpoint order, `configured`, model IDs, routing endpoints, reasoning efforts, and known context windows; null lists absent rather than empty; malformed sources warn and publish no inventory; projection is idempotent. |
### 9.3 Engine and integration fixture matrix
@@ -2118,6 +2162,12 @@ Native IDs can collide, timestamps can be out of order, and a trace can contain
## 13. Change Log (Informative)
+### Version 2.0.0 — Draft (2026-10-18)
+
+- Incompatible: `model.inventory` endpoint `models` is now optional. AWF `null` model and routing lists are omitted as unknown instead of written as empty arrays, so readers can no longer assume every endpoint has a model list (T-UAS-075).
+- Accepted endpoint-local `null` routing lists without rejecting the rest of the inventory, and retained endpoint `configured`, routing `supportedReasoningEfforts`, and `contextWindowTokens` when supplied.
+- Retained serialization-format version 1.
+
### Version 1.12.0 — Draft (2026-10-18)
- Retained AWF ranked choices, eligible-choice and catalogue-overlap counts, selected, requested, and upstream endpoints, request-telemetry `unavailable` dimensions, HTTP status, and failure phase in `firewall.model_routing` when supplied (T-UAS-070).
From 4010fc341534755822aeb3454cd74adb3bb79c4a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Oct 2026 00:12:38 +0000
Subject: [PATCH 3/4] Mark model inventory changeset major; constrain
contextWindowTokens schema to positive safe integer
Co-authored-by: SivaKesava1 <11771739+SivaKesava1@users.noreply.github.com>
---
...ajor-preserve-awf-model-inventory-null-endpoints.md | 5 +++++
.../preserve-awf-model-inventory-null-endpoints.md | 5 -----
actions/setup/js/types/unified_session.d.ts | 7 ++++++-
actions/setup/js/unified_session_payload.test.cjs | 10 ++++++++++
docs/public/schemas/unified-session.schema.json | 2 +-
5 files changed, 22 insertions(+), 7 deletions(-)
create mode 100644 .changeset/major-preserve-awf-model-inventory-null-endpoints.md
delete mode 100644 .changeset/preserve-awf-model-inventory-null-endpoints.md
diff --git a/.changeset/major-preserve-awf-model-inventory-null-endpoints.md b/.changeset/major-preserve-awf-model-inventory-null-endpoints.md
new file mode 100644
index 00000000000..f8911b7a58b
--- /dev/null
+++ b/.changeset/major-preserve-awf-model-inventory-null-endpoints.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": major
+---
+
+Keep AWF model inventories in unified sessions when an unconfigured endpoint reports null `models` or `routing_models`. Breaking change (unified session specification 2.0.0): `model.inventory` endpoint `models` and `routingModels` may now be absent, meaning unknown; readers must not treat absence as an empty list. Null lists are now omitted as unknown instead of becoming empty arrays or rejecting the whole snapshot, and endpoint `configured`, routing `supportedReasoningEfforts`, and `contextWindowTokens` are retained.
diff --git a/.changeset/preserve-awf-model-inventory-null-endpoints.md b/.changeset/preserve-awf-model-inventory-null-endpoints.md
deleted file mode 100644
index 0c076bc4197..00000000000
--- a/.changeset/preserve-awf-model-inventory-null-endpoints.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"gh-aw": patch
----
-
-Keep AWF model inventories in unified sessions when an unconfigured endpoint reports null `models` or `routing_models`. Null lists are now omitted as unknown instead of becoming empty arrays or rejecting the whole snapshot, and endpoint `configured`, routing `supportedReasoningEfforts`, and `contextWindowTokens` are retained (unified session specification 2.0.0).
diff --git a/actions/setup/js/types/unified_session.d.ts b/actions/setup/js/types/unified_session.d.ts
index 11a201e85d7..99ceb271730 100644
--- a/actions/setup/js/types/unified_session.d.ts
+++ b/actions/setup/js/types/unified_session.d.ts
@@ -238,7 +238,12 @@ export interface ModelInventoryData {
supportedEndpoints?: string[];
/** Identifier-shaped reasoning efforts; absent means unknown; a supplied empty array stays empty. */
supportedReasoningEfforts?: string[];
- /** Positive context window size in tokens; absent means unknown. */
+ /**
+ * Positive context window size in tokens; absent means unknown.
+ * @asType integer
+ * @minimum 1
+ * @maximum 9007199254740991
+ */
contextWindowTokens?: number;
}[];
}[];
diff --git a/actions/setup/js/unified_session_payload.test.cjs b/actions/setup/js/unified_session_payload.test.cjs
index 057951ce7cf..b2559d8c9f2 100644
--- a/actions/setup/js/unified_session_payload.test.cjs
+++ b/actions/setup/js/unified_session_payload.test.cjs
@@ -165,6 +165,16 @@ describe("essential unified session payloads", () => {
expect(() => normalizeUnifiedSessionEvent({ type: "model.inventory", data: source })).toThrow("ERR_VALIDATION: Model inventory snapshot must contain valid endpoint model collections");
});
+ it("publishes contextWindowTokens as a positive safe integer in the unified schema", () => {
+ const source = { type: "model.inventory", data: { endpoints: [{ provider: "copilot", models: ["a"], routing_models: [{ model_id: "a", context_window_tokens: 1 }] }] } };
+ const [event] = mergeSessionSources([{ component: "agent", phase: "agent", path: "awf-reflect.json", events: [source] }]);
+ const validate = createSessionValidator("unified").event;
+ expect(validate(event), JSON.stringify(validate.errors)).toBe(true);
+ const withWindow = contextWindowTokens => ({ ...event, data: { endpoints: [{ ...event.data.endpoints[0], routingModels: [{ modelId: "a", contextWindowTokens }] }] } });
+ expect(validate(withWindow(Number.MAX_SAFE_INTEGER)), JSON.stringify(validate.errors)).toBe(true);
+ for (const contextWindowTokens of [0, 1.5, Number.MAX_SAFE_INTEGER + 1]) expect(validate(withWindow(contextWindowTokens)), String(contextWindowTokens)).toBe(false);
+ });
+
it("projects canonical MCP configuration through the shared metadata allowlist", () => {
const source = {
type: "mcp.configuration",
diff --git a/docs/public/schemas/unified-session.schema.json b/docs/public/schemas/unified-session.schema.json
index 4bee481face..f2ddc38af0d 100644
--- a/docs/public/schemas/unified-session.schema.json
+++ b/docs/public/schemas/unified-session.schema.json
@@ -802,7 +802,7 @@
"modelId": { "type": "string" },
"supportedEndpoints": { "type": "array", "items": { "type": "string" } },
"supportedReasoningEfforts": { "type": "array", "items": { "type": "string" }, "description": "Identifier-shaped reasoning efforts; absent means unknown; a supplied empty array stays empty." },
- "contextWindowTokens": { "type": "number", "description": "Positive context window size in tokens; absent means unknown." }
+ "contextWindowTokens": { "type": "integer", "description": "Positive context window size in tokens; absent means unknown.", "minimum": 1, "maximum": 9007199254740991 }
},
"required": ["modelId"],
"additionalProperties": false
From 90677d815dc34e3d74614fd49b60567d576ebf49 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 19 Oct 2026 01:17:28 +0000
Subject: [PATCH 4/4] Clarify unknown versus empty model inventories
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
---
.changeset/major-preserve-awf-model-inventory-null-endpoints.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.changeset/major-preserve-awf-model-inventory-null-endpoints.md b/.changeset/major-preserve-awf-model-inventory-null-endpoints.md
index f8911b7a58b..fdf09e76360 100644
--- a/.changeset/major-preserve-awf-model-inventory-null-endpoints.md
+++ b/.changeset/major-preserve-awf-model-inventory-null-endpoints.md
@@ -2,4 +2,4 @@
"gh-aw": major
---
-Keep AWF model inventories in unified sessions when an unconfigured endpoint reports null `models` or `routing_models`. Breaking change (unified session specification 2.0.0): `model.inventory` endpoint `models` and `routingModels` may now be absent, meaning unknown; readers must not treat absence as an empty list. Null lists are now omitted as unknown instead of becoming empty arrays or rejecting the whole snapshot, and endpoint `configured`, routing `supportedReasoningEfforts`, and `contextWindowTokens` are retained.
+Keep AWF model inventories in unified sessions when an unconfigured endpoint reports null `models` or `routing_models`. Breaking change (unified session specification 2.0.0): `model.inventory` endpoint `models` and `routingModels` may now be absent, meaning unknown; readers must not treat absence as an empty list. Explicit empty arrays still mean a known-empty inventory. Null lists are now omitted as unknown instead of becoming empty arrays or rejecting the whole snapshot, and endpoint `configured`, routing `supportedReasoningEfforts`, and `contextWindowTokens` are retained.