typeclaw 0.36.8 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +3 -2
- package/src/agent/index.ts +31 -11
- package/src/agent/live-sessions.ts +12 -0
- package/src/agent/model-fallback.ts +17 -15
- package/src/agent/model-overrides.ts +2 -2
- package/src/agent/session-meta.ts +10 -0
- package/src/agent/subagents.ts +11 -2
- package/src/agent/system-prompt.ts +9 -3
- package/src/agent/todo/continuation-policy.ts +6 -3
- package/src/agent/todo/continuation-wiring.ts +4 -2
- package/src/agent/todo/continuation.ts +3 -3
- package/src/agent/tools/todo/index.ts +27 -4
- package/src/bundled-plugins/agent-browser/index.ts +33 -108
- package/src/bundled-plugins/agent-browser/shim.ts +3 -94
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
- package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
- package/src/bundled-plugins/memory/README.md +80 -23
- package/src/bundled-plugins/memory/append-tool.ts +74 -53
- package/src/bundled-plugins/memory/citation-superset.ts +4 -0
- package/src/bundled-plugins/memory/citations.ts +54 -0
- package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
- package/src/bundled-plugins/memory/dreaming.ts +444 -21
- package/src/bundled-plugins/memory/index.ts +544 -400
- package/src/bundled-plugins/memory/load-memory.ts +87 -10
- package/src/bundled-plugins/memory/load-shards.ts +48 -22
- package/src/bundled-plugins/memory/memory-logger.ts +95 -106
- package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
- package/src/bundled-plugins/memory/parent-link.ts +33 -0
- package/src/bundled-plugins/memory/paths.ts +12 -0
- package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
- package/src/bundled-plugins/memory/references/load-references.ts +212 -0
- package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
- package/src/bundled-plugins/memory/search-tool.ts +282 -45
- package/src/bundled-plugins/memory/stream-events.ts +1 -0
- package/src/bundled-plugins/memory/stream-io.ts +28 -3
- package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
- package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
- package/src/bundled-plugins/memory/vector/config.ts +28 -0
- package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
- package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
- package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
- package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
- package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
- package/src/bundled-plugins/memory/vector/passages.ts +125 -0
- package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
- package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
- package/src/bundled-plugins/memory/vector/startup.ts +71 -0
- package/src/bundled-plugins/memory/vector/store.ts +203 -0
- package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
- package/src/channels/router.ts +239 -40
- package/src/cli/incomplete-init.ts +57 -0
- package/src/cli/init.ts +143 -12
- package/src/cli/inspect.ts +11 -5
- package/src/cli/model.ts +112 -34
- package/src/cli/restart.ts +24 -0
- package/src/cli/start.ts +24 -0
- package/src/cli/tunnel.ts +53 -8
- package/src/config/config.ts +110 -19
- package/src/config/index.ts +5 -1
- package/src/config/models-mutation.ts +29 -11
- package/src/config/providers-mutation.ts +2 -2
- package/src/config/providers.ts +146 -12
- package/src/container/shared.ts +9 -0
- package/src/container/start.ts +87 -4
- package/src/cron/consumer.ts +13 -7
- package/src/hostd/models.ts +64 -0
- package/src/hostd/paths.ts +6 -0
- package/src/hostd/portbroker-manager.ts +2 -2
- package/src/init/checkpoint.ts +201 -0
- package/src/init/dockerfile.ts +121 -34
- package/src/init/gitignore.ts +7 -7
- package/src/init/index.ts +41 -9
- package/src/init/models-dev.ts +96 -21
- package/src/init/oauth-login.ts +3 -3
- package/src/init/progress.ts +29 -0
- package/src/init/validate-api-key.ts +4 -0
- package/src/inspect/index.ts +13 -6
- package/src/inspect/item-list.ts +11 -2
- package/src/inspect/live-list.ts +65 -0
- package/src/inspect/open-item.ts +22 -1
- package/src/inspect/session-list.ts +29 -0
- package/src/models/embedding-model.ts +114 -0
- package/src/models/transformers-version.ts +55 -0
- package/src/plugin/types.ts +3 -0
- package/src/portbroker/container-server.ts +23 -0
- package/src/portbroker/forward-request-bus.ts +35 -0
- package/src/portbroker/forward-result-bus.ts +2 -3
- package/src/portbroker/hostd-client.ts +182 -36
- package/src/portbroker/index.ts +6 -1
- package/src/portbroker/protocol.ts +9 -2
- package/src/run/channel-session-factory.ts +11 -1
- package/src/run/index.ts +41 -7
- package/src/server/command-runner.ts +24 -1
- package/src/server/index.ts +42 -8
- package/src/shared/index.ts +2 -0
- package/src/shared/protocol.ts +31 -0
- package/src/skills/typeclaw-channels/SKILL.md +4 -4
- package/src/skills/typeclaw-config/SKILL.md +2 -2
- package/src/skills/typeclaw-memory/SKILL.md +3 -1
- package/src/skills/typeclaw-permissions/SKILL.md +3 -3
- package/src/skills/typeclaw-skills/SKILL.md +1 -1
- package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
- package/src/tunnels/providers/cloudflare-quick.ts +65 -7
- package/src/tunnels/upstream-probe.ts +25 -0
- package/typeclaw.schema.json +156 -67
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
- package/src/portbroker/bind-with-forward.ts +0 -102
package/src/cli/tunnel.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { select, text, password, isCancel, cancel, log } from '@clack/prompts'
|
|
|
5
5
|
import { defineCommand } from 'citty'
|
|
6
6
|
|
|
7
7
|
import { loadConfigSync, validateConfig } from '@/config'
|
|
8
|
-
import { resolveHostPort, resolveTuiToken } from '@/container'
|
|
8
|
+
import { CONTAINER_PORT, resolveHostPort, resolveTuiToken } from '@/container'
|
|
9
9
|
import { appendOrReplaceEnvKey, findAgentDir, hasEnvKey, isInitialized } from '@/init'
|
|
10
10
|
import type { ClientMessage, ServerMessage, TunnelLogsServerMessage, TunnelSnapshot } from '@/shared'
|
|
11
11
|
import type { TunnelConfig, TunnelFor, TunnelProvider } from '@/tunnels'
|
|
@@ -921,13 +921,32 @@ async function withTuiSocket<T>(
|
|
|
921
921
|
}
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
-
async function resolveWsUrl(
|
|
924
|
+
async function resolveWsUrl(
|
|
925
|
+
cwd: string,
|
|
926
|
+
input?: string,
|
|
927
|
+
pathname = '/',
|
|
928
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
929
|
+
): Promise<LiveResult<string>> {
|
|
925
930
|
try {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
931
|
+
if (input !== undefined) {
|
|
932
|
+
const url = new URL(input)
|
|
933
|
+
url.pathname = pathname
|
|
934
|
+
return { ok: true, value: url.toString() }
|
|
930
935
|
}
|
|
936
|
+
// In-container short-circuit: when the agent runs `typeclaw tunnel …` from
|
|
937
|
+
// inside its own container (the only way it CAN run it), `docker` is not on
|
|
938
|
+
// $PATH, so the host-side discovery below (resolveHostPort/resolveTuiToken,
|
|
939
|
+
// which both shell out to `docker`) fails and the websocket connect aborts
|
|
940
|
+
// with the opaque `[object ErrorEvent]`. typeclaw's `docker run` sets
|
|
941
|
+
// TYPECLAW_CONTAINER_NAME (always) and TYPECLAW_TUI_TOKEN (when configured),
|
|
942
|
+
// and the agent's WS server listens on CONTAINER_PORT on the container
|
|
943
|
+
// loopback — so we can dial directly without docker. Mirrors the same
|
|
944
|
+
// short-circuit in src/cron/bridge.ts (resolveInContainerUrl).
|
|
945
|
+
const inContainer = resolveInContainerWsUrl(env, pathname)
|
|
946
|
+
if (inContainer !== null) return { ok: true, value: inContainer }
|
|
947
|
+
const url = new URL(`ws://127.0.0.1:${await resolveHostPort({ cwd })}`)
|
|
948
|
+
const token = await resolveTuiToken({ cwd })
|
|
949
|
+
if (token !== null) url.searchParams.set('token', token)
|
|
931
950
|
url.pathname = pathname
|
|
932
951
|
return { ok: true, value: url.toString() }
|
|
933
952
|
} catch (err) {
|
|
@@ -935,6 +954,17 @@ async function resolveWsUrl(cwd: string, input?: string, pathname = '/'): Promis
|
|
|
935
954
|
}
|
|
936
955
|
}
|
|
937
956
|
|
|
957
|
+
// Returns null on the host stage (TYPECLAW_CONTAINER_NAME unset), where the
|
|
958
|
+
// docker-based discovery in resolveWsUrl is the right path.
|
|
959
|
+
export function resolveInContainerWsUrl(env: NodeJS.ProcessEnv, pathname = '/'): string | null {
|
|
960
|
+
if (env.TYPECLAW_CONTAINER_NAME === undefined) return null
|
|
961
|
+
const url = new URL(`ws://127.0.0.1:${CONTAINER_PORT}`)
|
|
962
|
+
const token = env.TYPECLAW_TUI_TOKEN
|
|
963
|
+
if (token !== undefined && token !== '') url.searchParams.set('token', token)
|
|
964
|
+
url.pathname = pathname
|
|
965
|
+
return url.toString()
|
|
966
|
+
}
|
|
967
|
+
|
|
938
968
|
function waitForOpen(ws: WebSocket, timeoutMs: number): Promise<void> {
|
|
939
969
|
return new Promise((resolve, reject) => {
|
|
940
970
|
const timer = setTimeout(() => reject(new Error('timed out connecting to agent websocket')), timeoutMs)
|
|
@@ -948,15 +978,30 @@ function waitForOpen(ws: WebSocket, timeoutMs: number): Promise<void> {
|
|
|
948
978
|
)
|
|
949
979
|
ws.addEventListener(
|
|
950
980
|
'error',
|
|
951
|
-
(
|
|
981
|
+
(event) => {
|
|
952
982
|
clearTimeout(timer)
|
|
953
|
-
reject(
|
|
983
|
+
reject(new Error(describeWsErrorEvent(event)))
|
|
954
984
|
},
|
|
955
985
|
{ once: true },
|
|
956
986
|
)
|
|
957
987
|
})
|
|
958
988
|
}
|
|
959
989
|
|
|
990
|
+
// A WebSocket 'error' listener fires with an ErrorEvent, NOT an Error. Passing
|
|
991
|
+
// it straight to a catch site that does `String(err)` yields the useless
|
|
992
|
+
// `[object ErrorEvent]`. Pull the real message out (`.message`, or the nested
|
|
993
|
+
// `.error`) so failures read like `Expected 101 status code` / connection
|
|
994
|
+
// refused instead.
|
|
995
|
+
function describeWsErrorEvent(event: unknown): string {
|
|
996
|
+
if (event instanceof Error) return event.message
|
|
997
|
+
if (typeof event === 'object' && event !== null) {
|
|
998
|
+
const { message, error } = event as { message?: unknown; error?: unknown }
|
|
999
|
+
if (typeof message === 'string' && message !== '') return message
|
|
1000
|
+
if (error instanceof Error && error.message !== '') return error.message
|
|
1001
|
+
}
|
|
1002
|
+
return 'websocket connection failed'
|
|
1003
|
+
}
|
|
1004
|
+
|
|
960
1005
|
function waitForServerMessage(
|
|
961
1006
|
ws: WebSocket,
|
|
962
1007
|
timeoutMs: number,
|
package/src/config/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { accessSync, constants as fsConstants, readFileSync, statSync, writeFile
|
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
3
|
import { isAbsolute, join, posix, resolve } from 'node:path'
|
|
4
4
|
|
|
5
|
-
import type { Model } from '@mariozechner/pi-ai'
|
|
5
|
+
import type { KnownApi, Model } from '@mariozechner/pi-ai'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
|
|
8
8
|
import { channelsSchema, SEEDED_GITHUB_EVENT_ALLOWLISTS } from '@/channels/schema'
|
|
@@ -13,9 +13,12 @@ import { secretFieldSchema } from '@/secrets/resolve'
|
|
|
13
13
|
import {
|
|
14
14
|
DEFAULT_MODEL_REF,
|
|
15
15
|
KNOWN_PROVIDERS,
|
|
16
|
+
isKnownModelRef,
|
|
17
|
+
isModelRef,
|
|
16
18
|
listKnownModelRefs,
|
|
17
19
|
type KnownModelRef,
|
|
18
|
-
type
|
|
20
|
+
type ModelRef,
|
|
21
|
+
providerForModelRef,
|
|
19
22
|
} from './providers'
|
|
20
23
|
|
|
21
24
|
const CONFIG_FILE = 'typeclaw.json'
|
|
@@ -574,16 +577,55 @@ const tunnelsArraySchema = z
|
|
|
574
577
|
}
|
|
575
578
|
})
|
|
576
579
|
|
|
577
|
-
|
|
580
|
+
const customModelCostSchema = z
|
|
581
|
+
.object({
|
|
582
|
+
input: z.number().optional(),
|
|
583
|
+
output: z.number().optional(),
|
|
584
|
+
cacheRead: z.number().optional(),
|
|
585
|
+
cacheWrite: z.number().optional(),
|
|
586
|
+
})
|
|
587
|
+
.catchall(z.unknown())
|
|
588
|
+
|
|
589
|
+
export const customModelMetaSchema = z
|
|
590
|
+
.object({
|
|
591
|
+
name: z.string().min(1).optional(),
|
|
592
|
+
reasoning: z.boolean().optional(),
|
|
593
|
+
input: z.array(z.string().min(1)).optional(),
|
|
594
|
+
contextWindow: z.number().optional(),
|
|
595
|
+
maxTokens: z.number().optional(),
|
|
596
|
+
cost: customModelCostSchema.optional(),
|
|
597
|
+
})
|
|
598
|
+
.catchall(z.unknown())
|
|
599
|
+
|
|
600
|
+
export type CustomModelMeta = z.infer<typeof customModelMetaSchema>
|
|
601
|
+
|
|
602
|
+
export const customModelsSchema = z.record(z.string().min(1), customModelMetaSchema).default({})
|
|
603
|
+
|
|
604
|
+
export type CustomModels = z.infer<typeof customModelsSchema>
|
|
605
|
+
|
|
606
|
+
const customModelRefSchema = z.string().refine((ref) => isModelRef(ref), {
|
|
607
|
+
message: 'model ref must be "<known-provider>/<model-id>" with a known provider',
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
const singleModelRef = z.union([z.enum(knownModelRefs), customModelRefSchema])
|
|
611
|
+
|
|
612
|
+
function asModelRef(value: string): ModelRef {
|
|
613
|
+
if (isModelRef(value)) return value
|
|
614
|
+
throw new Error(`Invalid model ref: ${value}`)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// `models` maps a profile name to one or more model refs. Curated refs keep
|
|
618
|
+
// editor autocomplete through the enum branch, while custom refs are allowed
|
|
619
|
+
// when they target a known provider.
|
|
578
620
|
// `default` profile is mandatory; every other profile is optional and falls
|
|
579
621
|
// back to `default` at resolution time (see `resolveProfile`).
|
|
580
622
|
//
|
|
581
|
-
// Each value is either a single
|
|
623
|
+
// Each value is either a single model ref or a non-empty array of refs
|
|
582
624
|
// forming a fallback chain: when a turn against the first ref fails (hard
|
|
583
625
|
// throw or a soft provider error), the runtime disposes the failed session
|
|
584
626
|
// and replays the same prompt against the next ref. Schema accepts both
|
|
585
627
|
// shapes for ergonomics; the parsed value is always normalised to a
|
|
586
|
-
// non-empty array so downstream consumers read a uniform `
|
|
628
|
+
// non-empty array so downstream consumers read a uniform `ModelRef[]`.
|
|
587
629
|
//
|
|
588
630
|
// Profile names are open strings; the runtime recognizes a handful of
|
|
589
631
|
// well-known names by convention (`default`, `fast`, `deep`, `vision`) but
|
|
@@ -596,9 +638,9 @@ const tunnelsArraySchema = z
|
|
|
596
638
|
// `persistMigratedConfig`), so every downstream consumer sees the new shape.
|
|
597
639
|
const modelRefOrChainSchema = z
|
|
598
640
|
.union([
|
|
599
|
-
|
|
641
|
+
singleModelRef,
|
|
600
642
|
z
|
|
601
|
-
.array(
|
|
643
|
+
.array(singleModelRef)
|
|
602
644
|
.min(1)
|
|
603
645
|
// Reject exact duplicates in a chain — retrying the same ref after the
|
|
604
646
|
// same class of failure is almost certainly a config typo, and silently
|
|
@@ -609,7 +651,7 @@ const modelRefOrChainSchema = z
|
|
|
609
651
|
message: 'models chain must not contain duplicate refs',
|
|
610
652
|
}),
|
|
611
653
|
])
|
|
612
|
-
.transform((value) => (Array.isArray(value) ? value : [value]))
|
|
654
|
+
.transform((value) => (Array.isArray(value) ? value : [value]).map((ref) => asModelRef(ref)))
|
|
613
655
|
export const modelsSchema = z
|
|
614
656
|
.record(z.string().min(1), modelRefOrChainSchema)
|
|
615
657
|
.refine((m) => 'default' in m, { message: 'models.default is required' })
|
|
@@ -617,7 +659,7 @@ export const modelsSchema = z
|
|
|
617
659
|
// Zod's `z.record(..., refine)` doesn't refine the inferred type. The
|
|
618
660
|
// `default` key is schema-enforced, so we narrow it here to spare every
|
|
619
661
|
// consumer the `T | undefined` assertion noise.
|
|
620
|
-
export type Models = Record<string,
|
|
662
|
+
export type Models = Record<string, ModelRef[]> & { default: ModelRef[] }
|
|
621
663
|
|
|
622
664
|
export const configSchema = z
|
|
623
665
|
.object({
|
|
@@ -626,9 +668,10 @@ export const configSchema = z
|
|
|
626
668
|
// `default(() => ...)` ensures every parsed config has at least
|
|
627
669
|
// `models.default`. Direct `.default({ default: ... })` would short-circuit
|
|
628
670
|
// the refinement, so we lean on the lazy thunk form. The default value is
|
|
629
|
-
// shaped to match the post-transform output (always `
|
|
671
|
+
// shaped to match the post-transform output (always `ModelRef[]`),
|
|
630
672
|
// not the user-facing input shape.
|
|
631
|
-
models: modelsSchema.default(() => ({ default: [DEFAULT_MODEL_REF] })) as unknown as z.ZodType<Models>,
|
|
673
|
+
models: modelsSchema.default(() => ({ default: [asModelRef(DEFAULT_MODEL_REF)] })) as unknown as z.ZodType<Models>,
|
|
674
|
+
customModels: customModelsSchema,
|
|
632
675
|
// Defaults to `[]` so the field can be omitted from `typeclaw.json` (no
|
|
633
676
|
// host paths exposed) without failing the whole config load. `typeclaw
|
|
634
677
|
// init` omits this field so users don't see noise for the empty case.
|
|
@@ -656,12 +699,58 @@ export const configSchema = z
|
|
|
656
699
|
|
|
657
700
|
export type Config = z.infer<typeof configSchema>
|
|
658
701
|
|
|
659
|
-
export function resolveModel(ref: KnownModelRef
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
702
|
+
export function resolveModel(ref: KnownModelRef | ModelRef | string): Model<KnownApi> {
|
|
703
|
+
const providerId = providerForModelRef(ref)
|
|
704
|
+
const modelId = ref.slice(providerId.length + 1)
|
|
705
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
706
|
+
if (isKnownModelRef(ref)) {
|
|
707
|
+
const model = (provider.models as Record<string, Model<KnownApi>>)[modelId]
|
|
708
|
+
if (model !== undefined) return model
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (!isModelRef(ref)) {
|
|
712
|
+
throw new Error(`Invalid model ref "${ref}". Expected "<known-provider>/<model-id>".`)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const templateModelId = Object.keys(provider.models)[0]
|
|
716
|
+
if (templateModelId === undefined) {
|
|
717
|
+
throw new Error(`Provider ${providerId} has no curated models to use as a transport template`)
|
|
718
|
+
}
|
|
719
|
+
const template = (provider.models as Record<string, Model<KnownApi>>)[templateModelId]
|
|
720
|
+
if (template === undefined) {
|
|
721
|
+
throw new Error(`Provider ${providerId} has no curated models to use as a transport template`)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const meta = getConfig().customModels[ref]
|
|
725
|
+
return {
|
|
726
|
+
id: modelId,
|
|
727
|
+
provider: providerId,
|
|
728
|
+
baseUrl: provider.baseUrl ?? template.baseUrl,
|
|
729
|
+
api: template.api,
|
|
730
|
+
name: meta?.name ?? modelId,
|
|
731
|
+
reasoning: meta?.reasoning ?? false,
|
|
732
|
+
input: resolveCustomModelInput(meta?.input),
|
|
733
|
+
contextWindow: meta?.contextWindow ?? template.contextWindow,
|
|
734
|
+
maxTokens: meta?.maxTokens ?? template.maxTokens,
|
|
735
|
+
cost: resolveCustomModelCost(meta?.cost),
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function resolveCustomModelInput(input: readonly string[] | undefined): Model<KnownApi>['input'] {
|
|
740
|
+
if (input === undefined) return ['text']
|
|
741
|
+
const supported = input.filter(
|
|
742
|
+
(value): value is Model<KnownApi>['input'][number] => value === 'text' || value === 'image',
|
|
743
|
+
)
|
|
744
|
+
return supported.length > 0 ? supported : ['text']
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function resolveCustomModelCost(cost: CustomModelMeta['cost']): Model<KnownApi>['cost'] {
|
|
748
|
+
return {
|
|
749
|
+
input: cost?.input ?? 0,
|
|
750
|
+
output: cost?.output ?? 0,
|
|
751
|
+
cacheRead: cost?.cacheRead ?? 0,
|
|
752
|
+
cacheWrite: cost?.cacheWrite ?? 0,
|
|
753
|
+
}
|
|
665
754
|
}
|
|
666
755
|
|
|
667
756
|
// Resolves a profile name (e.g. `fast`, `deep`, `vision`) to its fallback
|
|
@@ -672,8 +761,8 @@ export function resolveModel(ref: KnownModelRef): Model<'openai-completions'> |
|
|
|
672
761
|
// session is created with first. Callers that don't implement fallback can
|
|
673
762
|
// keep reading `ref`; fallback-aware callers iterate `refs`.
|
|
674
763
|
export type ResolvedProfile = {
|
|
675
|
-
ref:
|
|
676
|
-
refs:
|
|
764
|
+
ref: ModelRef
|
|
765
|
+
refs: ModelRef[]
|
|
677
766
|
profile: string
|
|
678
767
|
fellBackToDefault: boolean
|
|
679
768
|
}
|
|
@@ -790,6 +879,7 @@ export type FieldEffect = 'applied' | 'restart-required' | 'ignored'
|
|
|
790
879
|
export const FIELD_EFFECTS: Record<string, FieldEffect> = {
|
|
791
880
|
$schema: 'ignored',
|
|
792
881
|
models: 'applied',
|
|
882
|
+
customModels: 'applied',
|
|
793
883
|
port: 'restart-required',
|
|
794
884
|
mounts: 'restart-required',
|
|
795
885
|
mcpServers: 'restart-required',
|
|
@@ -885,6 +975,7 @@ export function extractPluginConfigs(raw: unknown): Record<string, unknown> {
|
|
|
885
975
|
'$schema',
|
|
886
976
|
'port',
|
|
887
977
|
'models',
|
|
978
|
+
'customModels',
|
|
888
979
|
'mounts',
|
|
889
980
|
'plugins',
|
|
890
981
|
'alias',
|
package/src/config/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export {
|
|
|
2
2
|
buildConfigMigrationCommitMessage,
|
|
3
3
|
config,
|
|
4
4
|
configSchema,
|
|
5
|
+
customModelMetaSchema,
|
|
6
|
+
customModelsSchema,
|
|
5
7
|
DEFAULT_PLUGINS,
|
|
6
8
|
dockerSchema,
|
|
7
9
|
dockerfileSchema,
|
|
@@ -29,6 +31,8 @@ export {
|
|
|
29
31
|
type Config,
|
|
30
32
|
type ConfigChange,
|
|
31
33
|
type ConfigReloadDiff,
|
|
34
|
+
type CustomModelMeta,
|
|
35
|
+
type CustomModels,
|
|
32
36
|
type DockerConfig,
|
|
33
37
|
type DockerfileConfig,
|
|
34
38
|
type GitConfig,
|
|
@@ -43,5 +47,5 @@ export {
|
|
|
43
47
|
type ResolvedProfile,
|
|
44
48
|
type ValidateConfigResult,
|
|
45
49
|
} from './config'
|
|
46
|
-
export { type KnownModelRef, type KnownProviderId } from './providers'
|
|
50
|
+
export { type KnownModelRef, type KnownProviderId, type ModelRef } from './providers'
|
|
47
51
|
export { createConfigReloadable, type CreateConfigReloadableOptions } from './reloadable'
|
|
@@ -3,13 +3,16 @@ import { join } from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
import { commitSystemFileSync } from '@/git/system-commit'
|
|
5
5
|
|
|
6
|
-
import { configSchema, loadConfigSyncOrDefaults, validateConfig } from './config'
|
|
6
|
+
import { configSchema, loadConfigSyncOrDefaults, validateConfig, type CustomModelMeta } from './config'
|
|
7
7
|
import {
|
|
8
8
|
KNOWN_PROVIDERS,
|
|
9
|
+
isKnownModelRef,
|
|
10
|
+
isModelRef,
|
|
9
11
|
listKnownModelRefs,
|
|
10
12
|
providerForModelRef,
|
|
11
13
|
type KnownModelRef,
|
|
12
14
|
type KnownProviderId,
|
|
15
|
+
type ModelRef,
|
|
13
16
|
} from './providers'
|
|
14
17
|
import { isProviderConfigured, listConfiguredProviders } from './providers-mutation'
|
|
15
18
|
|
|
@@ -20,8 +23,8 @@ export type ModelProfileEntry = {
|
|
|
20
23
|
// Head of the fallback chain. Kept under the legacy `ref` name so callers
|
|
21
24
|
// that only care about the active model (the common case) don't need to
|
|
22
25
|
// dereference `refs[0]`. The chain itself is exposed as `refs`.
|
|
23
|
-
ref:
|
|
24
|
-
refs:
|
|
26
|
+
ref: ModelRef
|
|
27
|
+
refs: ModelRef[]
|
|
25
28
|
providerId: KnownProviderId
|
|
26
29
|
// Credential status for every provider referenced by the chain. The chain's
|
|
27
30
|
// overall status is `available` only when every entry resolves; otherwise
|
|
@@ -67,7 +70,7 @@ export function listModelProfiles(cwd: string, env: NodeJS.ProcessEnv = process.
|
|
|
67
70
|
return out
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
function uniqueProviders(refs: ReadonlyArray<
|
|
73
|
+
function uniqueProviders(refs: ReadonlyArray<ModelRef>): KnownProviderId[] {
|
|
71
74
|
const seen = new Set<KnownProviderId>()
|
|
72
75
|
const out: KnownProviderId[] = []
|
|
73
76
|
for (const r of refs) {
|
|
@@ -103,9 +106,7 @@ export function listRegisteredModelRefs(cwd: string, env: NodeJS.ProcessEnv = pr
|
|
|
103
106
|
return listKnownModelRefs().filter((ref) => registered.has(providerForModelRef(ref)))
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
export
|
|
107
|
-
return (listKnownModelRefs() as ReadonlyArray<string>).includes(value)
|
|
108
|
-
}
|
|
109
|
+
export { isKnownModelRef }
|
|
109
110
|
|
|
110
111
|
// `set` is the canonical mutation for both creating a new profile and updating
|
|
111
112
|
// an existing one (mirrors how `models.<profile>` works in the schema).
|
|
@@ -115,6 +116,7 @@ export function isKnownModelRef(value: string): value is KnownModelRef {
|
|
|
115
116
|
export type SetProfileOptions = {
|
|
116
117
|
force?: boolean
|
|
117
118
|
env?: NodeJS.ProcessEnv
|
|
119
|
+
meta?: CustomModelMeta
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
export function setProfile(
|
|
@@ -127,7 +129,7 @@ export function setProfile(
|
|
|
127
129
|
if (trimmed.length === 0) {
|
|
128
130
|
return { ok: false, reason: 'Profile name cannot be empty.' }
|
|
129
131
|
}
|
|
130
|
-
if (!
|
|
132
|
+
if (!isModelRef(ref)) {
|
|
131
133
|
return {
|
|
132
134
|
ok: false,
|
|
133
135
|
reason: `Unknown model "${ref}". Run \`typeclaw model list --available\` to see valid options.`,
|
|
@@ -143,7 +145,8 @@ export function setProfile(
|
|
|
143
145
|
|
|
144
146
|
const existingBefore = readModelsRaw(cwd)
|
|
145
147
|
const verb = existingBefore !== null && trimmed in existingBefore ? 'set' : 'add'
|
|
146
|
-
|
|
148
|
+
const customModel = !isKnownModelRef(ref) && options.meta !== undefined ? { ref, meta: options.meta } : undefined
|
|
149
|
+
return writeProfile(cwd, trimmed, ref, `model: ${verb} ${trimmed} → ${ref}`, customModel)
|
|
147
150
|
}
|
|
148
151
|
|
|
149
152
|
// `add` is just `set` with a uniqueness guard; users who want "update" should
|
|
@@ -189,19 +192,26 @@ export function removeProfile(cwd: string, profile: string): ModelMutationResult
|
|
|
189
192
|
return writeModels(cwd, next, `model: remove ${profile}`)
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
function writeProfile(
|
|
195
|
+
function writeProfile(
|
|
196
|
+
cwd: string,
|
|
197
|
+
profile: string,
|
|
198
|
+
ref: ModelRef,
|
|
199
|
+
message: string,
|
|
200
|
+
customModel?: { ref: ModelRef; meta: CustomModelMeta },
|
|
201
|
+
): ModelMutationResult {
|
|
193
202
|
const existing = readModelsRaw(cwd)
|
|
194
203
|
const next: Record<string, string | string[]> = existing === null ? { default: ref } : { ...existing, [profile]: ref }
|
|
195
204
|
if (existing === null && profile !== 'default') {
|
|
196
205
|
next.default = ref
|
|
197
206
|
}
|
|
198
|
-
return writeModels(cwd, next, message)
|
|
207
|
+
return writeModels(cwd, next, message, customModel)
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
function writeModels(
|
|
202
211
|
cwd: string,
|
|
203
212
|
models: Record<string, string | string[]>,
|
|
204
213
|
commitMessage: string,
|
|
214
|
+
customModel?: { ref: ModelRef; meta: CustomModelMeta },
|
|
205
215
|
): ModelMutationResult {
|
|
206
216
|
const path = join(cwd, CONFIG_FILE)
|
|
207
217
|
let parsed: Record<string, unknown>
|
|
@@ -215,6 +225,10 @@ function writeModels(
|
|
|
215
225
|
return { ok: false, reason: `Failed to read ${CONFIG_FILE}: ${(error as Error).message}` }
|
|
216
226
|
}
|
|
217
227
|
parsed.models = models
|
|
228
|
+
if (customModel !== undefined) {
|
|
229
|
+
const existingCustomModels = isObjectRecord(parsed.customModels) ? parsed.customModels : {}
|
|
230
|
+
parsed.customModels = { ...existingCustomModels, [customModel.ref]: customModel.meta }
|
|
231
|
+
}
|
|
218
232
|
const check = configSchema.safeParse(parsed)
|
|
219
233
|
if (!check.success) {
|
|
220
234
|
return {
|
|
@@ -244,6 +258,10 @@ function writeModels(
|
|
|
244
258
|
return { ok: true }
|
|
245
259
|
}
|
|
246
260
|
|
|
261
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
262
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
263
|
+
}
|
|
264
|
+
|
|
247
265
|
// Returns the raw `models` block from disk in its on-disk shape: each value
|
|
248
266
|
// is `string | string[]` (the user-facing schema). Writers preserve whichever
|
|
249
267
|
// shape was already present for profiles they don't touch — converting a
|
|
@@ -5,7 +5,7 @@ import { providerKeyDefaultEnv } from '@/secrets/defaults'
|
|
|
5
5
|
import type { ProviderCredential, Providers } from '@/secrets/schema'
|
|
6
6
|
|
|
7
7
|
import { type Models, loadConfigSync } from './config'
|
|
8
|
-
import { KNOWN_PROVIDERS, type
|
|
8
|
+
import { KNOWN_PROVIDERS, type KnownProviderId, type ModelRef, providerForModelRef } from './providers'
|
|
9
9
|
|
|
10
10
|
// Where a configured credential resolves from at runtime. Reported by
|
|
11
11
|
// `typeclaw provider list` so users can tell whether their key is coming from
|
|
@@ -230,7 +230,7 @@ function refTargetsProvider(ref: string, providerId: string): boolean {
|
|
|
230
230
|
return ref.startsWith(`${providerId}/`)
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
function safeProviderForRef(ref:
|
|
233
|
+
function safeProviderForRef(ref: ModelRef): KnownProviderId | null {
|
|
234
234
|
try {
|
|
235
235
|
return providerForModelRef(ref)
|
|
236
236
|
} catch {
|