Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
20 changes: 20 additions & 0 deletions src/components/MessageModel.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MessageModel message={message} isTranscriptMode={false} />,
80,
)
expect(rendered).toContain('jev-router → glm-5.3-flash')
})
62 changes: 26 additions & 36 deletions src/components/MessageModel.tsx
Original file line number Diff line number Diff line change
@@ -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 = <Text dimColor={true}>{message.message.model}</Text>;
$[0] = message.message.model;
$[1] = t2;
} else {
t2 = $[1];
}
let t3;
if ($[2] !== t1 || $[3] !== t2) {
t3 = <Box minWidth={t1}>{t2}</Box>;
$[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 (
<Box minWidth={stringWidth(display) + 8}>
<Text dimColor>{display}</Text>
</Box>
)
}
2 changes: 1 addition & 1 deletion src/components/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions src/services/api/openaiShim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | AsyncIterable<Record<string, unknown>>
const message = stream
? (await Array.fromAsync(result as AsyncIterable<Record<string, unknown>>))[0]?.message as Record<string, unknown>
: result as Record<string, unknown>
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
Expand Down
37 changes: 33 additions & 4 deletions src/services/api/openaiShim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -1429,6 +1445,7 @@ function createWarmingHintController(
async function* openaiStreamToAnthropic(
response: Response,
model: string,
routingMetadata?: { selected_model: string },
signal?: AbortSignal,
warmingHint?: WarmingHintController,
advertisedToolNames: readonly string[] = [],
Expand Down Expand Up @@ -1484,6 +1501,7 @@ async function* openaiStreamToAnthropic(
role: 'assistant',
content: [],
model,
...(routingMetadata && { metadata: routingMetadata }),
stop_reason: null,
stop_sequence: null,
usageReported: false,
Expand Down Expand Up @@ -2762,6 +2780,16 @@ class OpenAIShimMessages {
throw e
}
httpResponse = response
const routingMetadata = selectedModelMetadata(
response,
request.resolvedModel,
request.baseUrl,
)

const withRoutingMetadata = <T extends object>(message: T): T =>
routingMetadata
? { ...message, metadata: routingMetadata }
: message

if (params.stream) {
const isResponsesStream = response.url?.includes('/responses')
Expand All @@ -2779,6 +2807,7 @@ class OpenAIShimMessages {
: openaiStreamToAnthropic(
response,
request.resolvedModel,
routingMetadata,
options?.signal,
warmingHint,
advertisedToolNames,
Expand Down Expand Up @@ -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<any>(response)
return self._convertNonStreamingResponse(
return withRoutingMetadata(self._convertNonStreamingResponse(
data,
request.resolvedModel,
advertisedToolNames,
recoverableToolNames,
)
))
}

const textBody = await readSuccessfulResponseText(response).catch(
Expand Down
Loading