typeclaw 0.37.1 → 0.37.2
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/package.json +2 -1
- package/src/agent/index.ts +9 -1
- package/src/agent/proactive-next-step-nudge.ts +11 -0
- package/src/agent/session-origin.ts +21 -0
- package/src/channels/router.ts +77 -17
- package/src/cli/fuzzy-filter.ts +32 -0
- package/src/cli/init.ts +6 -0
- package/src/cli/model.ts +2 -0
- package/src/cli/provider.ts +3 -0
- package/src/config/providers.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typeclaw",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"homepage": "https://github.com/typeclaw/typeclaw#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/typeclaw/typeclaw/issues"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"format:check": "oxfmt --check .",
|
|
38
38
|
"check": "bun run typecheck && bun run lint && bun run format:check",
|
|
39
39
|
"test": "bun test --parallel",
|
|
40
|
+
"dev:docs": "cd docs && bun run dev",
|
|
40
41
|
"generate:schema": "bun run scripts/generate-schema.ts",
|
|
41
42
|
"debug:prompt": "bun run scripts/dump-system-prompt.ts",
|
|
42
43
|
"postinstall": "bun run scripts/generate-schema.ts"
|
package/src/agent/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { loadMemory } from '@/bundled-plugins/memory/load-memory'
|
|
|
15
15
|
import type { ChannelRouter } from '@/channels/router'
|
|
16
16
|
import type { ReactionRef } from '@/channels/types'
|
|
17
17
|
import { getConfig, resolveModel, resolveProfile } from '@/config'
|
|
18
|
-
import { defaultThinkingLevelForRef, providerForModelRef, type ModelRef } from '@/config/providers'
|
|
18
|
+
import { defaultThinkingLevelForRef, isOpenAiFamilyRef, providerForModelRef, type ModelRef } from '@/config/providers'
|
|
19
19
|
import { renderMcpCatalog } from '@/mcp/catalog'
|
|
20
20
|
import type { McpManager } from '@/mcp/manager'
|
|
21
21
|
import { createMcpDispatcherTools, MCP_DISPATCHER_TOOL_NAMES } from '@/mcp/tools'
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
wrapSystemTool,
|
|
48
48
|
zodToToolParameters,
|
|
49
49
|
} from './plugin-tools'
|
|
50
|
+
import { PROACTIVE_NEXT_STEP_NUDGE } from './proactive-next-step-nudge'
|
|
50
51
|
import { createReloadTool } from './reload-tool'
|
|
51
52
|
import type { RestartHandoffOrigin } from './restart-handoff'
|
|
52
53
|
import type { SubagentBashPolicy } from './reviewer-bash-policy'
|
|
@@ -277,6 +278,7 @@ export async function createSessionWithDispose(options: CreateSessionOptions = {
|
|
|
277
278
|
...(options.mcpManager !== undefined ? { mcpManager: options.mcpManager } : {}),
|
|
278
279
|
...(options.subagentRegistry !== undefined ? { subagentRegistry: options.subagentRegistry } : {}),
|
|
279
280
|
...(options.suppressSystemMemory !== undefined ? { suppressSystemMemory: options.suppressSystemMemory } : {}),
|
|
281
|
+
...(isOpenAiFamilyRef(activeRef) ? { proactiveNextStepNudge: true } : {}),
|
|
280
282
|
})
|
|
281
283
|
|
|
282
284
|
const getOrigin: () => SessionOrigin | undefined =
|
|
@@ -957,6 +959,7 @@ export type CreateResourceLoaderOptions = {
|
|
|
957
959
|
// from `memory.vector.enabled` — vector is restart-required, so the boot
|
|
958
960
|
// snapshot is coherent with the per-turn injection decision.
|
|
959
961
|
suppressSystemMemory?: boolean
|
|
962
|
+
proactiveNextStepNudge?: boolean
|
|
960
963
|
}
|
|
961
964
|
|
|
962
965
|
// Origins where the operator-facing DEFAULT_SYSTEM_PROMPT, git-nudge, and the
|
|
@@ -1020,6 +1023,7 @@ export type SystemPromptComposition = {
|
|
|
1020
1023
|
roleContext?: SessionRoleContext
|
|
1021
1024
|
mcpCatalog?: string
|
|
1022
1025
|
gitNudge: string
|
|
1026
|
+
proactiveNextStepNudge?: string
|
|
1023
1027
|
memorySection: string
|
|
1024
1028
|
}
|
|
1025
1029
|
|
|
@@ -1065,6 +1069,9 @@ export function composeSystemPrompt(parts: SystemPromptComposition): string {
|
|
|
1065
1069
|
if (parts.gitNudge !== '') {
|
|
1066
1070
|
prompt = `${prompt}\n\n${parts.gitNudge}`
|
|
1067
1071
|
}
|
|
1072
|
+
if (parts.proactiveNextStepNudge !== undefined && parts.proactiveNextStepNudge !== '') {
|
|
1073
|
+
prompt = `${prompt}\n\n${parts.proactiveNextStepNudge}`
|
|
1074
|
+
}
|
|
1068
1075
|
if (parts.memorySection !== '') {
|
|
1069
1076
|
prompt = `${prompt}\n\n${parts.memorySection}`
|
|
1070
1077
|
}
|
|
@@ -1164,6 +1171,7 @@ export async function createResourceLoader(options: CreateResourceLoaderOptions
|
|
|
1164
1171
|
? { mcpCatalog: renderMcpCatalog(options.mcpManager.listServers()) }
|
|
1165
1172
|
: {}),
|
|
1166
1173
|
gitNudge,
|
|
1174
|
+
...(options.proactiveNextStepNudge === true ? { proactiveNextStepNudge: PROACTIVE_NEXT_STEP_NUDGE } : {}),
|
|
1167
1175
|
memorySection,
|
|
1168
1176
|
})
|
|
1169
1177
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const PROACTIVE_NEXT_STEP_NUDGE_TITLE = '## Proactive and requested next-step guidance'
|
|
2
|
+
|
|
3
|
+
// GPT-only prompt text is intentionally absent from scripts/dump-system-prompt.ts
|
|
4
|
+
// token accounting because the dump tooling renders with non-GPT placeholders.
|
|
5
|
+
export const PROACTIVE_NEXT_STEP_NUDGE = [
|
|
6
|
+
PROACTIVE_NEXT_STEP_NUDGE_TITLE,
|
|
7
|
+
'',
|
|
8
|
+
'GPT/OpenAI-family behavior nudge: when the user asks for work and a reasonable or necessary next step is obvious, do not ask for permission or confirmation before doing it. Do the next step when it makes sense, especially when it is necessary to complete the task well. Avoid empty optional follow-up CTAs such as “if you want, I can also …”; either take the useful next action or end with the completed result.',
|
|
9
|
+
'',
|
|
10
|
+
'When the user explicitly asks for suggestions, options, alternatives, or what to do next, answer that request directly with concrete next-step suggestions instead of treating suggestions as an unwanted follow-up CTA.',
|
|
11
|
+
].join('\n')
|
|
@@ -384,6 +384,27 @@ function renderChannelOrigin(
|
|
|
384
384
|
)
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// Discord renders no GFM tables — a raw `| a | b |` block shows as literal
|
|
388
|
+
// pipes. The discord-bot adapter rewrites BARE pipe tables into aligned
|
|
389
|
+
// inline-code rows for readability, but it skips any table inside a ``` /
|
|
390
|
+
// ~~~ fence (a fenced table is literal text by CommonMark). Models that have
|
|
391
|
+
// "learned" Discord mangles tables defensively wrap them in a fence, which is
|
|
392
|
+
// exactly what disables the auto-conversion — so the table renders ragged
|
|
393
|
+
// anyway. Tell the model to emit tables bare and let the adapter format them.
|
|
394
|
+
if (origin.adapter === 'discord-bot') {
|
|
395
|
+
lines.push(
|
|
396
|
+
'',
|
|
397
|
+
'**Emit Markdown tables as bare `| a | b |` blocks — never inside a code',
|
|
398
|
+
'fence.** Discord does not render Markdown tables, so this session',
|
|
399
|
+
'auto-reformats a bare pipe table (a `|`-row followed by a `|---|`',
|
|
400
|
+
'alignment row) into aligned, readable columns before it sends. That',
|
|
401
|
+
'reformatting only fires on raw Markdown: the moment you wrap the table in',
|
|
402
|
+
'a ``` or ~~~ fence it is treated as literal text and lands as ragged pipes.',
|
|
403
|
+
'So write the table directly in your reply with no surrounding fence. Use',
|
|
404
|
+
'fences only for actual code or output you want shown verbatim.',
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
387
408
|
const conversationLine = renderConversationLine(origin)
|
|
388
409
|
if (conversationLine !== null) lines.push('', conversationLine)
|
|
389
410
|
|
package/src/channels/router.ts
CHANGED
|
@@ -3358,23 +3358,6 @@ export function createChannelRouter(options: CreateChannelRouterOptions): Channe
|
|
|
3358
3358
|
live.skippedTurn = null
|
|
3359
3359
|
logger.info(`[channels] ${live.keyId} skip_contested_by_send recovering reply`)
|
|
3360
3360
|
}
|
|
3361
|
-
// A send landed this turn, but the model may have posted a `continue: true`
|
|
3362
|
-
// progress reply, kept working, then ENDED with its final answer as plain
|
|
3363
|
-
// prose — never calling a channel tool again. The terminal-reply abort fires
|
|
3364
|
-
// only for a `channel_reply` WITHOUT `continue: true`, so that `stopReason:
|
|
3365
|
-
// 'stop'` text leaf is left undelivered and unguarded (the false-receipt
|
|
3366
|
-
// guard is github-only). The discriminator is leaf IDENTITY: only when the
|
|
3367
|
-
// turn-end `stop` leaf is a DIFFERENT entry than the one in place at the last
|
|
3368
|
-
// send did the model produce fresh post-reply prose. A leaf unchanged since
|
|
3369
|
-
// the send is narration the model emitted with/before the reply that already
|
|
3370
|
-
// landed — suppress it, as before.
|
|
3371
|
-
if (live.successfulChannelSends > successfulSendsBeforePrompt) {
|
|
3372
|
-
maybeNudgeContinuationWillingness(live)
|
|
3373
|
-
const trailing = recoverableAssistantText(live.session)
|
|
3374
|
-
if (trailing === null || trailing.source !== 'leaf') return
|
|
3375
|
-
if (live.session.sessionManager.getLeafEntry()?.id === live.lastSendLeafId) return
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
3361
|
const postEmptyTurnFallback = async (cause: string): Promise<void> => {
|
|
3379
3362
|
logger.warn(`[channels] ${live.keyId} empty_turn_fallback cause=${cause}`)
|
|
3380
3363
|
const result = await send(
|
|
@@ -3392,6 +3375,49 @@ export function createChannelRouter(options: CreateChannelRouterOptions): Channe
|
|
|
3392
3375
|
}
|
|
3393
3376
|
}
|
|
3394
3377
|
|
|
3378
|
+
// A send landed this turn, but the model may have posted a `continue: true`
|
|
3379
|
+
// progress reply, kept working, then ENDED with its final answer as plain
|
|
3380
|
+
// prose — never calling a channel tool again. The terminal-reply abort fires
|
|
3381
|
+
// only for a `channel_reply` WITHOUT `continue: true`, so that `stopReason:
|
|
3382
|
+
// 'stop'` text leaf is left undelivered and unguarded (the false-receipt
|
|
3383
|
+
// guard is github-only). The discriminator is leaf IDENTITY: only when the
|
|
3384
|
+
// turn-end `stop` leaf is a DIFFERENT entry than the one in place at the last
|
|
3385
|
+
// send did the model produce fresh post-reply prose. A leaf unchanged since
|
|
3386
|
+
// the send is narration the model emitted with/before the reply that already
|
|
3387
|
+
// landed — suppress it, as before.
|
|
3388
|
+
if (live.successfulChannelSends > successfulSendsBeforePrompt) {
|
|
3389
|
+
maybeNudgeContinuationWillingness(live)
|
|
3390
|
+
const trailing = recoverableAssistantText(live.session)
|
|
3391
|
+
if (trailing === null || trailing.source !== 'leaf') {
|
|
3392
|
+
// A `continue: true` status reply landed, then the turn stranded on an
|
|
3393
|
+
// unanswered `toolUse` (the post-tool follow-up never produced an
|
|
3394
|
+
// assistant message — aborted loop / cancelled stream). The promised
|
|
3395
|
+
// work never finished, so the user is left with a bare "checking now…"
|
|
3396
|
+
// and nothing after it. Re-prompt the same logical turn so the model
|
|
3397
|
+
// completes its investigation and actually replies, instead of ending
|
|
3398
|
+
// in silence. On retry-exhaustion post the fallback rather than
|
|
3399
|
+
// returning silently — a retry turn that re-sends a status and re-strands
|
|
3400
|
+
// on the same no-prose shape must not deadair the user. Any postable
|
|
3401
|
+
// pre-tool/mid-turn prose is suppressed here as before (it was narration
|
|
3402
|
+
// that accompanied the already-landed reply); only the no-prose strand
|
|
3403
|
+
// gets a retry-or-fallback.
|
|
3404
|
+
if (leafIsStrandedToolUse(live.session) && live.currentTurnAuthorId !== null) {
|
|
3405
|
+
if (live.emptyTurnRetries < MAX_EMPTY_TURN_RETRIES) {
|
|
3406
|
+
live.emptyTurnRetries++
|
|
3407
|
+
logger.warn(
|
|
3408
|
+
`[channels] ${live.keyId} empty_turn_retry attempt=${live.emptyTurnRetries}/${MAX_EMPTY_TURN_RETRIES} ` +
|
|
3409
|
+
`cause=stranded_toolUse_after_send`,
|
|
3410
|
+
)
|
|
3411
|
+
live.pendingSystemReminders.push(EMPTY_TURN_RETRY_NUDGE)
|
|
3412
|
+
} else {
|
|
3413
|
+
await postEmptyTurnFallback('stranded_toolUse_retries_exhausted')
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
return
|
|
3417
|
+
}
|
|
3418
|
+
if (live.session.sessionManager.getLeafEntry()?.id === live.lastSendLeafId) return
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3395
3421
|
let candidate = recoverableAssistantText(live.session)
|
|
3396
3422
|
// A `length` leaf is recovered ONLY when stripping leaked `<think>…</think>`
|
|
3397
3423
|
// spans actually removed something AND leaves a postable reply. The removal
|
|
@@ -4961,6 +4987,40 @@ function assistantLeafStopReason(session: AgentSession): 'length' | 'error' | 'a
|
|
|
4961
4987
|
return undefined
|
|
4962
4988
|
}
|
|
4963
4989
|
|
|
4990
|
+
// True when the branch ends on an UNANSWERED `toolUse` that left NO postable
|
|
4991
|
+
// prose — the model called a tool and the upstream pi-agent-core post-tool
|
|
4992
|
+
// follow-up never produced an assistant message (the loop was aborted, or the
|
|
4993
|
+
// follow-up stream cancelled). Two leaf shapes carry this signature: the leaf
|
|
4994
|
+
// IS a `toolUse` assistant, or the leaf is a `toolResult` whose nearest
|
|
4995
|
+
// assistant ancestor (reached before any user message) is `toolUse`. The
|
|
4996
|
+
// no-prose requirement is the discriminator from a model that narrated a reply
|
|
4997
|
+
// alongside its tool call and DID land a real send this turn (that trailing
|
|
4998
|
+
// `toolUse` is delivered narration, not a stranded promise — leave it alone).
|
|
4999
|
+
// Keys on the model having INTENDED to keep working with nothing yet said; used
|
|
5000
|
+
// to re-prompt a turn that strands mid-work after a `continue: true` status
|
|
5001
|
+
// reply instead of ending in silence.
|
|
5002
|
+
function leafIsStrandedToolUse(session: AgentSession): boolean {
|
|
5003
|
+
const leaf = session.sessionManager.getLeafEntry()
|
|
5004
|
+
if (!leaf || leaf.type !== 'message') return false
|
|
5005
|
+
if (leaf.message.role === 'assistant') {
|
|
5006
|
+
return leaf.message.stopReason === 'toolUse' && visibleAssistantText(leaf.message).trim() === ''
|
|
5007
|
+
}
|
|
5008
|
+
if (leaf.message.role !== 'toolResult') return false
|
|
5009
|
+
let cursor: { parentId: string | null } | undefined = leaf
|
|
5010
|
+
for (let depth = 0; depth < 32 && cursor?.parentId; depth++) {
|
|
5011
|
+
const parent = session.sessionManager.getEntry(cursor.parentId)
|
|
5012
|
+
if (!parent) return false
|
|
5013
|
+
if (parent.type === 'message') {
|
|
5014
|
+
if (parent.message.role === 'assistant') {
|
|
5015
|
+
return parent.message.stopReason === 'toolUse' && visibleAssistantText(parent.message).trim() === ''
|
|
5016
|
+
}
|
|
5017
|
+
if (parent.message.role === 'user') return false
|
|
5018
|
+
}
|
|
5019
|
+
cursor = parent
|
|
5020
|
+
}
|
|
5021
|
+
return false
|
|
5022
|
+
}
|
|
5023
|
+
|
|
4964
5024
|
function visibleAssistantText(message: AssistantMessage): string {
|
|
4965
5025
|
return message.content
|
|
4966
5026
|
.filter((block) => block.type === 'text')
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Option } from '@clack/prompts'
|
|
2
|
+
|
|
3
|
+
type FuzzyFilter<Value> = (search: string, option: Option<Value>) => boolean
|
|
4
|
+
|
|
5
|
+
function optionHaystack<Value>(option: Option<Value>): string {
|
|
6
|
+
const label = option.label ?? String(option.value)
|
|
7
|
+
const hint = option.hint ?? ''
|
|
8
|
+
return `${label} ${String(option.value)} ${hint}`.toLowerCase()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isSubsequence(query: string, haystack: string): boolean {
|
|
12
|
+
let i = 0
|
|
13
|
+
for (let j = 0; j < haystack.length && i < query.length; j++) {
|
|
14
|
+
if (haystack[j] === query[i]) i++
|
|
15
|
+
}
|
|
16
|
+
return i === query.length
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Splitting the query on whitespace lets "gpt 5.5" match "GPT-5.5 Turbo": each
|
|
20
|
+
// token is matched independently as a subsequence, so the "-" inside "GPT-5.5"
|
|
21
|
+
// no longer breaks the search the way a plain substring "gpt 5.5" would. Tokens
|
|
22
|
+
// are order-independent so "turbo gpt" finds "GPT Turbo" too.
|
|
23
|
+
export function fuzzyMatch<Value>(search: string, option: Option<Value>): boolean {
|
|
24
|
+
const tokens = search.toLowerCase().split(/\s+/).filter(Boolean)
|
|
25
|
+
if (tokens.length === 0) return true
|
|
26
|
+
const haystack = optionHaystack(option)
|
|
27
|
+
return tokens.every((token) => isSubsequence(token, haystack))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const fuzzyFilter: FuzzyFilter<unknown> = fuzzyMatch
|
|
31
|
+
|
|
32
|
+
export type { FuzzyFilter }
|
package/src/cli/init.ts
CHANGED
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
type KeyValidationResult,
|
|
67
67
|
} from '@/init/validate-api-key'
|
|
68
68
|
|
|
69
|
+
import { fuzzyMatch } from './fuzzy-filter'
|
|
69
70
|
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
70
71
|
import { CANCEL_SYMBOL, promptPrivateKeyPem } from './prompt-pem'
|
|
71
72
|
import {
|
|
@@ -1098,6 +1099,7 @@ async function pickVendor(
|
|
|
1098
1099
|
const choice = await autocomplete({
|
|
1099
1100
|
message: 'Pick an LLM provider',
|
|
1100
1101
|
placeholder: 'Type to search…',
|
|
1102
|
+
filter: fuzzyMatch,
|
|
1101
1103
|
options: vendors.map((id) => ({
|
|
1102
1104
|
value: id,
|
|
1103
1105
|
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
@@ -1120,6 +1122,7 @@ async function pickProviderVariant(
|
|
|
1120
1122
|
const choice = await autocomplete<KnownProviderId>({
|
|
1121
1123
|
message: `Pick a ${KNOWN_PROVIDER_VENDORS[vendorId].name} option`,
|
|
1122
1124
|
placeholder: 'Type to search…',
|
|
1125
|
+
filter: fuzzyMatch,
|
|
1123
1126
|
options: variants.map((id) => {
|
|
1124
1127
|
const hint = variantHint(vendorId, id)
|
|
1125
1128
|
return hint !== undefined
|
|
@@ -1145,6 +1148,7 @@ async function pickModelForProvider(
|
|
|
1145
1148
|
const choice = await autocomplete<string>({
|
|
1146
1149
|
message: `Pick a ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
1147
1150
|
placeholder: 'Type to search…',
|
|
1151
|
+
filter: fuzzyMatch,
|
|
1148
1152
|
options: candidates.map((o) => ({
|
|
1149
1153
|
value: o.ref,
|
|
1150
1154
|
label: formatModelLabel(o),
|
|
@@ -1191,6 +1195,7 @@ async function pickVisionVendor(
|
|
|
1191
1195
|
const choice = await autocomplete<KnownProviderVendorId | 'skip'>({
|
|
1192
1196
|
message: 'Your model is text-only. Pick a provider for the `vision` profile (used for image input)',
|
|
1193
1197
|
placeholder: 'Type to search…',
|
|
1198
|
+
filter: fuzzyMatch,
|
|
1194
1199
|
options: [
|
|
1195
1200
|
...vendors.map((id) => ({
|
|
1196
1201
|
value: id as KnownProviderVendorId | 'skip',
|
|
@@ -1223,6 +1228,7 @@ async function pickVisionModel(
|
|
|
1223
1228
|
const choice = await autocomplete<string>({
|
|
1224
1229
|
message: `Pick a vision-capable ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
1225
1230
|
placeholder: 'Type to search…',
|
|
1231
|
+
filter: fuzzyMatch,
|
|
1226
1232
|
options: candidates.map((o) => ({
|
|
1227
1233
|
value: o.ref,
|
|
1228
1234
|
label: formatModelLabel(o),
|
package/src/cli/model.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { findAgentDir, isInitialized } from '@/init'
|
|
20
20
|
import { customModelMetaFromOption, fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
21
21
|
|
|
22
|
+
import { fuzzyMatch } from './fuzzy-filter'
|
|
22
23
|
import { runProviderAddFlow } from './provider'
|
|
23
24
|
import { c, done, errorLine } from './ui'
|
|
24
25
|
|
|
@@ -251,6 +252,7 @@ async function pickModelRef(cwd: string): Promise<PickedModelRef> {
|
|
|
251
252
|
const choice = await autocomplete<string>({
|
|
252
253
|
message: 'Pick a model',
|
|
253
254
|
placeholder: 'Type to search…',
|
|
255
|
+
filter: fuzzyMatch,
|
|
254
256
|
options: [
|
|
255
257
|
...modelOptions.map((option) => ({
|
|
256
258
|
value: option.ref,
|
package/src/cli/provider.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { findAgentDir, isInitialized } from '@/init'
|
|
24
24
|
import { makeOAuthLoginRunner } from '@/init/oauth-login'
|
|
25
25
|
|
|
26
|
+
import { fuzzyMatch } from './fuzzy-filter'
|
|
26
27
|
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
27
28
|
import { c, done, errorLine } from './ui'
|
|
28
29
|
|
|
@@ -285,6 +286,7 @@ async function pickVendorToAdd(): Promise<KnownProviderVendorId> {
|
|
|
285
286
|
const choice = await autocomplete<KnownProviderVendorId>({
|
|
286
287
|
message: 'Pick a provider to add',
|
|
287
288
|
placeholder: 'Type to search…',
|
|
289
|
+
filter: fuzzyMatch,
|
|
288
290
|
options: vendorIds.map((id) => ({
|
|
289
291
|
value: id,
|
|
290
292
|
label: KNOWN_PROVIDER_VENDORS[id].name,
|
|
@@ -305,6 +307,7 @@ async function pickVariantToAdd(vendorId: KnownProviderVendorId): Promise<KnownP
|
|
|
305
307
|
const choice = await autocomplete<KnownProviderId>({
|
|
306
308
|
message: `Pick a ${KNOWN_PROVIDER_VENDORS[vendorId].name} option`,
|
|
307
309
|
placeholder: 'Type to search…',
|
|
310
|
+
filter: fuzzyMatch,
|
|
308
311
|
options: variants.map((id) => {
|
|
309
312
|
const hint = variantHint(vendorId, id)
|
|
310
313
|
return hint !== undefined
|
package/src/config/providers.ts
CHANGED
|
@@ -991,9 +991,12 @@ function knownProviderForModelRef(ref: string): KnownProviderId | null {
|
|
|
991
991
|
//
|
|
992
992
|
// Anthropic, GLM, and Kimi don't share the padding behavior, so they keep the
|
|
993
993
|
// SDK default.
|
|
994
|
+
export function isOpenAiFamilyRef(ref: KnownModelRef | ModelRef | string): boolean {
|
|
995
|
+
return vendorForProviderId(providerForModelRef(ref)) === 'openai'
|
|
996
|
+
}
|
|
997
|
+
|
|
994
998
|
export function defaultThinkingLevelForRef(ref: KnownModelRef | ModelRef | string): 'low' | undefined {
|
|
995
|
-
|
|
996
|
-
if (providerId === 'openai' || providerId === 'openai-codex') return 'low'
|
|
999
|
+
if (isOpenAiFamilyRef(ref)) return 'low'
|
|
997
1000
|
return undefined
|
|
998
1001
|
}
|
|
999
1002
|
|