diff --git a/README.md b/README.md
index cdec09958b..15b6223586 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,10 @@ approval flow.
## Agent Model Routing
+Ao usar `jev-router`, a CLI mostra o modelo físico escolhido em cada resposta
+como `jev-router → glm-5.3-flash` (exemplo). O alias solicitado permanece no
+protocolo; a escolha vem do metadado da resposta do roteador.
+
Verboo resolves subagent models against the authenticated `/models` catalog and
preserves the complete server-provided ID, including plan prefixes such as
`max/qwen3.6-27b`. Router-provided `agent_model_roles` are authoritative; the
diff --git a/package.json b/package.json
index f692c4b7c2..fb28850b75 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@verboo/code",
- "version": "0.15.30",
+ "version": "0.15.31",
"description": "Verboo Code — coding agent for the Verboo platform",
"type": "module",
"bin": {
diff --git a/src/components/MessageModel.test.tsx b/src/components/MessageModel.test.tsx
new file mode 100644
index 0000000000..6f50a79746
--- /dev/null
+++ b/src/components/MessageModel.test.tsx
@@ -0,0 +1,20 @@
+import { expect, test } from 'bun:test'
+import React from 'react'
+import { renderToString } from '../utils/staticRender.js'
+import { MessageModel } from './MessageModel.js'
+
+test('shows the physical Jev choice during a normal CLI conversation', async () => {
+ const message = {
+ type: 'assistant',
+ message: {
+ model: 'jev-router',
+ metadata: { selected_model: 'glm-5.3-flash' },
+ content: [{ type: 'text', text: 'ok' }],
+ },
+ }
+ const rendered = await renderToString(
+ ,
+ 80,
+ )
+ expect(rendered).toContain('jev-router → glm-5.3-flash')
+})
diff --git a/src/components/MessageModel.tsx b/src/components/MessageModel.tsx
index da05ad1fd5..b35b774d18 100644
--- a/src/components/MessageModel.tsx
+++ b/src/components/MessageModel.tsx
@@ -1,42 +1,32 @@
-import { c as _c } from "react-compiler-runtime";
-import React from 'react';
-import { stringWidth } from '../ink/stringWidth.js';
-import { Box, Text } from '../ink.js';
-import type { NormalizedMessage } from '../types/message.js';
+import React from 'react'
+import { stringWidth } from '../ink/stringWidth.js'
+import { Box, Text } from '../ink.js'
+import type { NormalizedMessage } from '../types/message.js'
type Props = {
message: NormalizedMessage;
isTranscriptMode: boolean;
};
-export function MessageModel(t0) {
- const $ = _c(5);
- const {
- message,
- isTranscriptMode
- } = t0;
- const shouldShowModel = isTranscriptMode && message.type === "assistant" && message.message.model && message.message.content.some(_temp);
- if (!shouldShowModel) {
- return null;
+export function MessageModel({ message, isTranscriptMode }: Props) {
+ if (
+ message.type !== 'assistant' ||
+ message.message.content.length === 0
+ ) {
+ return null
}
- const t1 = stringWidth(message.message.model) + 8;
- let t2;
- if ($[0] !== message.message.model) {
- t2 = {message.message.model};
- $[0] = message.message.model;
- $[1] = t2;
- } else {
- t2 = $[1];
- }
- let t3;
- if ($[2] !== t1 || $[3] !== t2) {
- t3 = {t2};
- $[2] = t1;
- $[3] = t2;
- $[4] = t3;
- } else {
- t3 = $[4];
- }
- return t3;
-}
-function _temp(c) {
- return c.type === "text";
+
+ const requested = message.message.model
+ const selected = message.message.metadata?.selected_model
+ const display =
+ typeof selected === 'string' && selected.length > 0
+ ? `${requested} → ${selected}`
+ : isTranscriptMode
+ ? requested
+ : null
+ if (!display) return null
+
+ return (
+
+ {display}
+
+ )
}
diff --git a/src/components/MessageRow.tsx b/src/components/MessageRow.tsx
index 4c47ffa240..0fbd24f490 100644
--- a/src/components/MessageRow.tsx
+++ b/src/components/MessageRow.tsx
@@ -218,7 +218,7 @@ function MessageRowImpl(t0) {
}
let t5;
if ($[34] !== displayMsg || $[35] !== isTranscriptMode) {
- t5 = isTranscriptMode && displayMsg.type === "assistant" && displayMsg.message.content.some(_temp) && (displayMsg.timestamp || displayMsg.message.model);
+ t5 = displayMsg.type === "assistant" && (isTranscriptMode && displayMsg.message.content.some(_temp) && (displayMsg.timestamp || displayMsg.message.model) || displayMsg.message.content.length > 0 && typeof displayMsg.message.metadata?.selected_model === "string");
$[34] = displayMsg;
$[35] = isTranscriptMode;
$[36] = t5;
diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts
index 65533de442..a35aca350b 100644
--- a/src/services/api/openaiShim.test.ts
+++ b/src/services/api/openaiShim.test.ts
@@ -315,6 +315,38 @@ test('adds Verboo session header only for Verboo router requests', async () => {
)
})
+test.each([false, true])('jev-router exposes the selected model in message metadata (stream=%s)', async stream => {
+ process.env.CLAUDE_CODE_USE_OPENAI = '1'
+ process.env.OPENAI_BASE_URL = VERBOO_ROUTER_URL
+ globalThis.fetch = (async () => {
+ const headers = { 'X-Verboo-Selected-Model': 'glm-5.3-flash' }
+ if (stream) {
+ return makeSseResponse(makeStreamChunks([
+ { choices: [{ delta: { content: 'ok' }, finish_reason: null }] },
+ { choices: [{ delta: {}, finish_reason: 'stop' }] },
+ ]), headers)
+ }
+ return new Response(JSON.stringify({
+ id: 'chatcmpl-selected',
+ model: 'jev-router',
+ choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }],
+ }), { headers: { ...headers, 'Content-Type': 'application/json' } })
+ }) as unknown as FetchType
+
+ const client = createOpenAIShimClient({}) as OpenAIShimClient
+ const result = await client.beta.messages.create({
+ model: 'jev-router',
+ messages: [{ role: 'user', content: 'hello' }],
+ max_tokens: 64,
+ stream,
+ }) as Record | AsyncIterable>
+ const message = stream
+ ? (await Array.fromAsync(result as AsyncIterable>))[0]?.message as Record
+ : result as Record
+ expect(message.model).toBe('jev-router')
+ expect(message.metadata).toEqual({ selected_model: 'glm-5.3-flash' })
+})
+
test('captures router rate limit headers from successful Verboo router responses', async () => {
process.env.CLAUDE_CODE_USE_OPENAI = '1'
process.env.OPENAI_BASE_URL = VERBOO_ROUTER_URL
diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts
index fdfd983efa..17395b26c4 100644
--- a/src/services/api/openaiShim.ts
+++ b/src/services/api/openaiShim.ts
@@ -130,6 +130,22 @@ const GITHUB_429_MAX_DELAY_SEC = 32
const GEMINI_API_HOST = 'generativelanguage.googleapis.com'
const VERBOO_SESSION_HEADER = 'X-Verboo-Session-Id'
const VERBOO_REQUEST_ID_HEADER = 'x-verboo-request-id'
+const VERBOO_SELECTED_MODEL_HEADER = 'x-verboo-selected-model'
+
+function selectedModelMetadata(
+ response: Response,
+ requestedModel: string,
+ baseUrl: string,
+): { selected_model: string } | undefined {
+ if (!isVerbooRouterUrl(baseUrl) || requestedModel.split('/').at(-1) !== 'jev-router') {
+ return undefined
+ }
+ const selected = response.headers.get(VERBOO_SELECTED_MODEL_HEADER)
+ if (!selected || selected.length > 128 || !/^[A-Za-z0-9._:/+-]+$/.test(selected)) {
+ return undefined
+ }
+ return { selected_model: selected }
+}
type ClientDiagnosticStage =
| 'client_decode'
@@ -1429,6 +1445,7 @@ function createWarmingHintController(
async function* openaiStreamToAnthropic(
response: Response,
model: string,
+ routingMetadata?: { selected_model: string },
signal?: AbortSignal,
warmingHint?: WarmingHintController,
advertisedToolNames: readonly string[] = [],
@@ -1484,6 +1501,7 @@ async function* openaiStreamToAnthropic(
role: 'assistant',
content: [],
model,
+ ...(routingMetadata && { metadata: routingMetadata }),
stop_reason: null,
stop_sequence: null,
usageReported: false,
@@ -2762,6 +2780,16 @@ class OpenAIShimMessages {
throw e
}
httpResponse = response
+ const routingMetadata = selectedModelMetadata(
+ response,
+ request.resolvedModel,
+ request.baseUrl,
+ )
+
+ const withRoutingMetadata = (message: T): T =>
+ routingMetadata
+ ? { ...message, metadata: routingMetadata }
+ : message
if (params.stream) {
const isResponsesStream = response.url?.includes('/responses')
@@ -2779,6 +2807,7 @@ class OpenAIShimMessages {
: openaiStreamToAnthropic(
response,
request.resolvedModel,
+ routingMetadata,
options?.signal,
warmingHint,
advertisedToolNames,
@@ -2827,24 +2856,24 @@ class OpenAIShimMessages {
recoverableToolNames,
)
}
- return self._convertNonStreamingResponse(
+ return withRoutingMetadata(self._convertNonStreamingResponse(
parsed,
request.resolvedModel,
advertisedToolNames,
recoverableToolNames,
- )
+ ))
}
}
const contentType = response.headers.get('content-type') ?? ''
if (contentType.includes('application/json')) {
const data = await readSuccessfulResponseJson(response)
- return self._convertNonStreamingResponse(
+ return withRoutingMetadata(self._convertNonStreamingResponse(
data,
request.resolvedModel,
advertisedToolNames,
recoverableToolNames,
- )
+ ))
}
const textBody = await readSuccessfulResponseText(response).catch(