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
34 changes: 21 additions & 13 deletions console/web/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@ import { useConversationsCtxOptional } from '@/lib/conversations-context'
import { formatStopReason } from '@/lib/format-stop-reason'
import { newMessageId } from '@/lib/session-id'
import { cn } from '@/lib/utils'
import type {
AssistantMessage,
Conversation,
FunctionCallMessage,
Message,
MessagePatch,
Mode,
ModelId,
ModelOption,
SystemMessage,
ThoughtMessage,
UserMessage,
import {
type AssistantMessage,
type Conversation,
DEFAULT_THINKING_LEVEL,
type FunctionCallMessage,
type Message,
type MessagePatch,
type Mode,
type ModelId,
type ModelOption,
type SystemMessage,
type ThinkingLevel,
type ThoughtMessage,
type UserMessage,
} from '@/types/chat'
import { Composer, type ComposerSubmitPayload } from './Composer'
import { ContextUsage } from './ContextUsage'
Expand Down Expand Up @@ -99,6 +101,9 @@ export function ChatView({
onCompactConversation,
}: ChatViewProps) {
const [isStreaming, setIsStreaming] = useState(false)
const [thinkingLevel, setThinkingLevel] = useState<ThinkingLevel>(
DEFAULT_THINKING_LEVEL,
)
const abortRef = useRef<AbortController | null>(null)
const [copied, setCopied] = useState(false)
const { functionEntries } = useFunctionsCatalog(backend.id)
Expand Down Expand Up @@ -303,7 +308,7 @@ export function ChatView({
payload.text || '(attachments only)',
conversation.mode,
model,
{ signal: controller.signal, sessionId, messageId },
{ signal: controller.signal, sessionId, messageId, thinkingLevel },
)) {
switch (event.kind) {
case 'thought-start': {
Expand Down Expand Up @@ -530,6 +535,7 @@ export function ChatView({
conversation.id,
conversation.mode,
conversation.model,
thinkingLevel,
sessionId,
contextWindow,
backend,
Expand Down Expand Up @@ -680,6 +686,8 @@ export function ChatView({
functionEntries={functionEntries}
permissionMode={approvalSettings.settings.mode}
permissionModeLoading={!approvalSettings.loaded}
thinkingLevel={thinkingLevel}
onThinkingLevelChange={setThinkingLevel}
onModeChange={(next) => onUpdateMode(conversation.id, next)}
onModelChange={(next) => onUpdateModel(conversation.id, next)}
onPermissionModeChange={(next) =>
Expand Down
2 changes: 2 additions & 0 deletions console/web/src/components/chat/Composer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ function ComposerHarness({
modelOptions={STORY_MODEL_OPTIONS}
functionEntries={STATIC_FUNCTIONS}
permissionMode="manual"
thinkingLevel="off"
onThinkingLevelChange={fn()}
onModeChange={setMode}
onModelChange={setModel}
onPermissionModeChange={fn()}
Expand Down
24 changes: 23 additions & 1 deletion console/web/src/components/chat/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { PermissionModePicker } from '@/components/permissions/PermissionModePic
import { Button } from '@/components/ui/Button'
import type { PermissionMode } from '@/lib/backend/approval-settings'
import type { FunctionEntry } from '@/lib/functions'
import type { Attachment, Mode, ModelId, ModelOption } from '@/types/chat'
import { Select } from '@/components/ui/Select'
import {
type Attachment,
type Mode,
type ModelId,
type ModelOption,
THINKING_LEVELS,
type ThinkingLevel,
} from '@/types/chat'
import { AttachmentButton } from './AttachmentButton'
import { AttachmentChip } from './AttachmentChip'
import { LexicalShell } from './LexicalShell'
Expand All @@ -28,8 +36,10 @@ interface ComposerProps {
*/
permissionMode: PermissionMode
permissionModeLoading?: boolean
thinkingLevel: ThinkingLevel
onModeChange: (next: Mode) => void
onModelChange: (next: ModelId) => void
onThinkingLevelChange: (next: ThinkingLevel) => void
onPermissionModeChange: (next: PermissionMode) => void
onSubmit: (payload: ComposerSubmitPayload) => void
onStop?: () => void
Expand All @@ -52,8 +62,10 @@ export function Composer({
catalogLoading,
permissionMode,
permissionModeLoading,
thinkingLevel,
onModeChange,
onModelChange,
onThinkingLevelChange,
onPermissionModeChange,
onSubmit,
onStop,
Expand Down Expand Up @@ -133,6 +145,16 @@ export function Composer({
disabled={inputDisabled || !!permissionModeLoading}
/>
<div className="flex-1 min-w-0" />
<Select<ThinkingLevel>
value={thinkingLevel}
options={THINKING_LEVELS.map((l) => ({
value: l,
label: l === 'off' ? 'thinking off' : `thinking ${l}`,
}))}
onChange={onThinkingLevelChange}
disabled={inputDisabled}
aria-label="thinking level"
/>
<ModelPicker
value={model}
options={modelOptions}
Expand Down
2 changes: 1 addition & 1 deletion console/web/src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function resolveEmptyState(
onInstallHarness: harnessStatus.install,
onRetryInstall: harnessStatus.retry,
onConfigureProvider: () => {
window.location.hash = '#/configuration/workers/harness'
window.location.hash = '#/configuration/workers/llm-router'
},
}

Expand Down
6 changes: 3 additions & 3 deletions console/web/src/components/chat/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
type ModelOption,
} from '@/types/chat'

// Deep link to the harness configuration entry in the workers/config editor,
// Deep link to the llm-router configuration entry in the workers/config editor,
// where api keys + per-provider settings are now edited (the bespoke
// per-provider dialog was retired in favour of the schema-driven form).
const HARNESS_CONFIG_HASH = '#/configuration/workers/harness'
const HARNESS_CONFIG_HASH = '#/configuration/workers/llm-router'

interface ModelPickerProps {
value: ModelId | null
Expand Down Expand Up @@ -47,7 +47,7 @@ export function ModelPicker({
// Optional: present in the app, absent in isolated Storybook renders.
const ctx = useConversationsCtxOptional()

// Providers present as harness workers (from harness::provider::list).
// Providers present as workers (from router::provider::list).
// Absent in Storybook or before the list resolves, in which case no empty
// provider groups or gears appear until the dynamic list arrives.
const presentIds = ctx?.presentProviders.map((p) => p.id) ?? []
Expand Down
7 changes: 6 additions & 1 deletion console/web/src/components/chat/ThoughtMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ function thoughtLabel(durationMs: number): string {

export function ThoughtMessage({ message, defaultOpen }: ThoughtMessageProps) {
const streaming = !!message.streaming
// Auto-open while the thought streams so reasoning is visible in real
// time; the flip back when streaming ends collapses it to its summary.
return (
<details className="iii-details group/thought" open={defaultOpen}>
<details
className="iii-details group/thought"
open={defaultOpen || streaming}
>
<summary
className={cn(
'inline-flex items-center gap-2 font-mono text-[12px] text-ink-faint hover:text-ink transition-colors lowercase select-none',
Expand Down
2 changes: 1 addition & 1 deletion console/web/src/hooks/use-model-picker-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { ModelOption } from '@/types/chat'
* shows present-but-unconfigured providers as gear groups). When the engine is
* unreachable (catalog fetch throws) the list stays empty.
*
* `presentProviders` comes from `harness::provider::list` so the picker can
* `presentProviders` comes from `router::provider::list` so the picker can
* surface providers that exist as workers but aren't configured yet.
* `refresh()` re-reads both on demand (after a provider `refresh_models`).
*
Expand Down
3 changes: 3 additions & 0 deletions console/web/src/lib/backend/real.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ async function* realStream(
provider,
model: modelId,
mode,
...(opts?.thinkingLevel && opts.thinkingLevel !== 'off'
? { thinking_level: opts.thinkingLevel }
: {}),
messages: [
{
role: 'user',
Expand Down
6 changes: 6 additions & 0 deletions console/web/src/lib/backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export type StreamEvent =

export interface ChatStreamOptions {
signal?: AbortSignal
/**
* Reasoning/thinking level for the turn. Sent to `run::start` as
* `thinking_level`; omitted when 'off' or absent. The provider degrades
* with a warning when the model can't honor it.
*/
thinkingLevel?: import('@/types/chat').ThinkingLevel
/** mean delay between assistant tokens, in ms */
meanDelayMs?: number
/**
Expand Down
6 changes: 3 additions & 3 deletions console/web/src/lib/harness-config-events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Fired after `configuration::set` succeeds for the harness entry. */
const HARNESS_CONFIG_ID = 'harness'
/** Fired after `configuration::set` succeeds for the llm-router entry (provider credentials + settings). */
const LLM_ROUTER_CONFIG_ID = 'llm-router'

type Listener = () => void

Expand All @@ -13,7 +13,7 @@ export function onHarnessConfigSaved(listener: Listener): () => void {
}

export function notifyHarnessConfigSaved(configId: string): void {
if (configId !== HARNESS_CONFIG_ID) return
if (configId !== LLM_ROUTER_CONFIG_ID) return
for (const listener of listeners) {
listener()
}
Expand Down
10 changes: 5 additions & 5 deletions console/web/src/lib/models-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { makeCatalogModelKey } from '@/lib/catalog-model-key'
import { getIiiClient } from '@/lib/iii-client'
import type { ModelOption } from '@/types/chat'

/** Wire shape returned by `models::list` over the iii bus. */
/** Wire shape returned by `router::models::list` over the iii bus. */
export interface CatalogModelRow {
id: string
provider: string
Expand All @@ -12,7 +12,7 @@ export interface CatalogModelRow {

export async function fetchModelsCatalog(): Promise<CatalogModelRow[]> {
const client = await getIiiClient()
const res = await client.call<{ models?: unknown }>('models::list', {})
const res = await client.call<{ models?: unknown }>('router::models::list', {})
const rows = res?.models
if (!Array.isArray(rows)) return []
const out: CatalogModelRow[] = []
Expand Down Expand Up @@ -50,7 +50,7 @@ export function catalogRowsToModelOptions(
* Ask each provider to re-pull its upstream model list into the catalog via
* `provider::<id>::refresh_models`. Best-effort and parallel — a provider
* that's offline or has no credential simply registers nothing. Callers
* re-read `models::list` afterwards to pick up the refreshed catalog.
* re-read `router::models::list` afterwards to pick up the refreshed catalog.
*/
export async function refreshProviderModels(
providers: readonly string[],
Expand Down Expand Up @@ -96,7 +96,7 @@ export async function subscribeModelChanges(
}
}

/** A provider declared to the harness, from `harness::provider::list`. */
/** A provider declared to the harness, from `router::provider::list`. */
export interface ProviderListEntry {
id: string
display_name: string
Expand All @@ -111,7 +111,7 @@ export interface ProviderListEntry {
export async function fetchProviderList(): Promise<ProviderListEntry[]> {
const client = await getIiiClient()
const res = await client.call<{ providers?: unknown }>(
'harness::provider::list',
'router::provider::list',
{},
)
const rows = res?.providers
Expand Down
2 changes: 1 addition & 1 deletion console/web/src/lib/providers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Client-side validation helpers + error normalization shared by provider
* surfaces. Credentials and per-provider settings are now edited through the
* schema-driven `configuration` form (harness entry), so the former
* schema-driven `configuration` form (llm-router entry), so the former
* `auth::*` / `provider_config::*` bus wrappers were removed — the
* `configuration::*` calls in `WorkersTab/api.ts` cover that path.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import {
saveDefaultAllowlist,
} from '@/lib/storage'

// Provider credentials + settings now live in the harness `configuration`
// Provider credentials + settings now live in the llm-router `configuration`
// entry, edited via the schema-driven form on the workers tab.
const HARNESS_CONFIG_HASH = '#/configuration/workers/harness'
const HARNESS_CONFIG_HASH = '#/configuration/workers/llm-router'

interface ConsoleSettingsTabProps {
theme: Theme
Expand Down
14 changes: 14 additions & 0 deletions console/web/src/types/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ export const MODES: { id: Mode; label: string }[] = [

export const DEFAULT_MODE: Mode = 'agent'

/** Reasoning effort sent to run::start as `thinking_level`; 'off' is omitted. */
export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh'

export const THINKING_LEVELS: ThinkingLevel[] = [
'off',
'minimal',
'low',
'medium',
'high',
'xhigh',
]

export const DEFAULT_THINKING_LEVEL: ThinkingLevel = 'off'

export type Role = 'user' | 'assistant' | 'thought' | 'function-call'

export interface Attachment {
Expand Down
14 changes: 1 addition & 13 deletions harness/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "harness",
"version": "0.5.7",
"private": true,
"description": "Node port of the iii harness stack: harness, approval-gate, turn-orchestrator, llm-budget, providers, and side-cars. Conversations persist in the external session-manager worker.",
"description": "Node port of the iii harness stack: harness, approval-gate, turn-orchestrator, llm-budget, and side-cars. Conversations persist in the external session-manager worker; LLM providers are standalone llm-router plugin workers.",
"license": "Apache-2.0",
"type": "module",
"engines": {
Expand All @@ -26,12 +26,6 @@
"dev:turn-orchestrator": "tsx src/turn-orchestrator/main.ts",
"dev:llm-budget": "tsx src/llm-budget/main.ts",
"dev:hook-fanout": "tsx src/hook-fanout/main.ts",
"dev:models-catalog": "tsx src/models-catalog/main.ts",
"dev:provider-anthropic": "tsx src/provider-anthropic/main.ts",
"dev:provider-openai": "tsx src/provider-openai/main.ts",
"dev:provider-kimi": "tsx src/provider-kimi/main.ts",
"dev:provider-lmstudio": "tsx src/provider-lmstudio/main.ts",
"dev:provider-llamacpp": "tsx src/provider-llamacpp/main.ts",
"dev:context-compaction": "tsx src/context-compaction/main.ts",
"dev:web": "tsx src/web/main.ts"
},
Expand All @@ -42,12 +36,6 @@
"iii-turn-orchestrator": "./dist/turn-orchestrator/main.js",
"iii-llm-budget": "./dist/llm-budget/main.js",
"iii-hook-fanout": "./dist/hook-fanout/main.js",
"iii-models-catalog": "./dist/models-catalog/main.js",
"iii-provider-anthropic": "./dist/provider-anthropic/main.js",
"iii-provider-openai": "./dist/provider-openai/main.js",
"iii-provider-kimi": "./dist/provider-kimi/main.js",
"iii-provider-lmstudio": "./dist/provider-lmstudio/main.js",
"iii-provider-llamacpp": "./dist/provider-llamacpp/main.js",
"iii-context-compaction": "./dist/context-compaction/main.js",
"iii-web": "./dist/web/main.js"
},
Expand Down
23 changes: 14 additions & 9 deletions harness/src/context-compaction/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,37 @@ export async function fetchModelLimit(
modelID: string,
): Promise<ResolvedModel> {
try {
// router::models::get: payload key is `id`, result is wrapped as
// `{ model }`, null on a catalog miss (the cold-window signal).
const entry = await iii.trigger<
unknown,
{
id?: string;
provider?: string;
context_window?: number;
max_output_tokens?: number;
model?: {
id?: string;
provider?: string;
context_window?: number;
max_output_tokens?: number;
} | null;
} | null
>({
function_id: 'models::get',
payload: { provider: providerID, model_id: modelID },
function_id: 'router::models::get',
payload: { provider: providerID, id: modelID },
timeoutMs: 5_000,
});

if (!entry) {
const model = entry?.model ?? null;
if (!model) {
logger.debug('model-resolver: model not found in catalog', { providerID, modelID });
return null;
}

return {
providerID,
modelID,
modelLimit: limitFromModel(entry),
modelLimit: limitFromModel(model),
};
} catch (err) {
logger.debug('model-resolver: models::get failed', {
logger.debug('model-resolver: router::models::get failed', {
providerID,
modelID,
err: String(err),
Expand Down
Loading
Loading