mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
// OpenCode 1.x models --verbose emits each provider/model ID followed by indented JSON.
|
|
|
|
import { z } from 'zod'
|
|
import type { AgentSessionModelOption } from '../../shared/agent-session/agent-session-wire'
|
|
import { assertJsonTextStructureWithinLimits } from '../../shared/json-stream/json-text-structure-limit'
|
|
|
|
export const OPENCODE_MODEL_LISTING_ARGS = ['models', '--verbose'] as const
|
|
|
|
// The default sentinel means no variant override.
|
|
const DEFAULT_VARIANT = 'default'
|
|
const MODEL_JSON_LIMITS = { structuralTokens: 64 * 1024, nestingDepth: 32 } as const
|
|
|
|
const modelMetaSchema = z.looseObject({
|
|
name: z.string().min(1).optional(),
|
|
variants: z.record(z.string(), z.unknown()).optional()
|
|
})
|
|
|
|
function variantLabel(variant: string): string {
|
|
return variant
|
|
.split(/[_-]/)
|
|
.map((part) => (part ? part.charAt(0).toUpperCase() + part.slice(1) : part))
|
|
.join(' ')
|
|
}
|
|
|
|
function modelOption(id: string, metadata: unknown): AgentSessionModelOption {
|
|
const meta = modelMetaSchema.safeParse(metadata)
|
|
const name = meta.success ? meta.data.name : undefined
|
|
const variants = meta.success ? Object.keys(meta.data.variants ?? {}) : []
|
|
const providerId = id.slice(0, id.indexOf('/'))
|
|
// This parser serves only OpenCode < 2.0, whose ACP session runs a listed default variant, else
|
|
// the first listed one; the listing keeps the runtime's variant order.
|
|
const runsVariant = variants.includes(DEFAULT_VARIANT) ? DEFAULT_VARIANT : variants[0]
|
|
return {
|
|
id,
|
|
label: name ? `${providerId}/${name}` : id,
|
|
// Listing proves availability, not the configured default.
|
|
isDefault: false,
|
|
declaredDefaultEffort: runsVariant ?? null,
|
|
...(variants.length > 0
|
|
? {
|
|
efforts: [...new Set([...variants, DEFAULT_VARIANT])].map((value) => ({
|
|
value,
|
|
label: variantLabel(value)
|
|
})),
|
|
defaultEffort: runsVariant
|
|
}
|
|
: { efforts: [] })
|
|
}
|
|
}
|
|
|
|
function isModelIdLine(line: string): boolean {
|
|
const slash = line.indexOf('/')
|
|
return slash > 0 && slash < line.length - 1 && !/\s/.test(line) && !line.startsWith('{')
|
|
}
|
|
|
|
/** Retain model IDs even when malformed metadata prevents an effort menu. */
|
|
export function parseOpenCodeModelListing(stdout: string): AgentSessionModelOption[] {
|
|
const lines = stdout.split(/\r?\n/)
|
|
const models = new Map<string, AgentSessionModelOption>()
|
|
// Pretty-printed objects end at a bare closing brace; empty objects fit on one line.
|
|
const closesObject = (line: string, first: boolean): boolean =>
|
|
line.trimEnd() === '}' || (first && line.trimEnd() === '{}')
|
|
for (let index = 0; index < lines.length; index++) {
|
|
const id = (lines[index] ?? '').trim()
|
|
if (!isModelIdLine(id)) {
|
|
continue
|
|
}
|
|
let metadata: unknown
|
|
if (lines[index + 1]?.startsWith('{')) {
|
|
const start = index + 1
|
|
let end = start
|
|
while (end < lines.length && !closesObject(lines[end] ?? '', end === start)) {
|
|
end++
|
|
}
|
|
const text = lines.slice(start, end + 1).join('\n')
|
|
index = end
|
|
try {
|
|
assertJsonTextStructureWithinLimits(text, MODEL_JSON_LIMITS)
|
|
metadata = JSON.parse(text)
|
|
} catch {
|
|
metadata = undefined
|
|
}
|
|
}
|
|
if (!models.has(id)) {
|
|
models.set(id, modelOption(id, metadata))
|
|
}
|
|
}
|
|
return [...models.values()]
|
|
}
|