Merge branch 'pr-1959' into dev

# Conflicts:
#	src/hooks/index.ts
#	src/plugin/event.ts
#	src/tools/delegate-task/sync-task.ts
This commit is contained in:
YeonGyu-Kim
2026-02-21 02:49:39 +09:00
39 changed files with 3911 additions and 41 deletions
+2
View File
@@ -34,6 +34,7 @@ export * from "./system-directive"
export * from "./agent-tool-restrictions"
export * from "./model-requirements"
export * from "./model-resolver"
export { normalizeFallbackModels } from "./model-resolver"
export { resolveModelPipeline } from "./model-resolution-pipeline"
export type {
ModelResolutionRequest,
@@ -58,3 +59,4 @@ export * from "./normalize-sdk-response"
export * from "./session-directory-resolver"
export * from "./prompt-tools"
export * from "./internal-initiator-marker"
export { SessionCategoryRegistry } from "./session-category-registry"
+37
View File
@@ -8,6 +8,7 @@ export type ModelResolutionRequest = {
intent?: {
uiSelectedModel?: string
userModel?: string
userFallbackModels?: string[]
categoryDefaultModel?: string
}
constraints: {
@@ -101,6 +102,42 @@ export function resolveModelPipeline(
})
}
//#when - user configured fallback_models, try them before hardcoded fallback chain
const userFallbackModels = intent?.userFallbackModels
if (userFallbackModels && userFallbackModels.length > 0) {
if (availableModels.size === 0) {
const connectedProviders = constraints.connectedProviders ?? connectedProvidersCache.readConnectedProvidersCache()
const connectedSet = connectedProviders ? new Set(connectedProviders) : null
if (connectedSet !== null) {
for (const model of userFallbackModels) {
attempted.push(model)
const parts = model.split("/")
if (parts.length >= 2) {
const provider = parts[0]
if (connectedSet.has(provider)) {
log("Model resolved via user fallback_models (connected provider)", { model })
return { model, provenance: "provider-fallback", attempted }
}
}
}
log("No connected provider found in user fallback_models, falling through to hardcoded chain")
}
} else {
for (const model of userFallbackModels) {
attempted.push(model)
const parts = model.split("/")
const providerHint = parts.length >= 2 ? [parts[0]] : undefined
const match = fuzzyMatchModel(model, availableModels, providerHint)
if (match) {
log("Model resolved via user fallback_models (availability confirmed)", { model: model, match })
return { model: match, provenance: "provider-fallback", attempted }
}
}
log("No available model found in user fallback_models, falling through to hardcoded chain")
}
}
if (fallbackChain && fallbackChain.length > 0) {
if (availableModels.size === 0) {
const connectedProviders = constraints.connectedProviders ?? connectedProvidersCache.readConnectedProvidersCache()
+13 -2
View File
@@ -22,6 +22,7 @@ export type ModelResolutionResult = {
export type ExtendedModelResolutionInput = {
uiSelectedModel?: string
userModel?: string
userFallbackModels?: string[]
categoryDefaultModel?: string
fallbackChain?: FallbackEntry[]
availableModels: Set<string>
@@ -44,9 +45,9 @@ export function resolveModel(input: ModelResolutionInput): string | undefined {
export function resolveModelWithFallback(
input: ExtendedModelResolutionInput,
): ModelResolutionResult | undefined {
const { uiSelectedModel, userModel, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input
const { uiSelectedModel, userModel, userFallbackModels, categoryDefaultModel, fallbackChain, availableModels, systemDefaultModel } = input
const resolved = resolveModelPipeline({
intent: { uiSelectedModel, userModel, categoryDefaultModel },
intent: { uiSelectedModel, userModel, userFallbackModels, categoryDefaultModel },
constraints: { availableModels },
policy: { fallbackChain, systemDefaultModel },
})
@@ -61,3 +62,13 @@ export function resolveModelWithFallback(
variant: resolved.variant,
}
}
/**
* Normalizes fallback_models config (which can be string or string[]) to string[]
* Centralized helper to avoid duplicated normalization logic
*/
export function normalizeFallbackModels(models: string | string[] | undefined): string[] | undefined {
if (!models) return undefined
if (typeof models === "string") return [models]
return models
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Session Category Registry
*
* Maintains a mapping of session IDs to their assigned categories.
* Used by runtime-fallback hook to lookup category-specific fallback_models.
*/
// Map of sessionID -> category name
const sessionCategoryMap = new Map<string, string>()
export const SessionCategoryRegistry = {
/**
* Register a session with its category
*/
register: (sessionID: string, category: string): void => {
sessionCategoryMap.set(sessionID, category)
},
/**
* Get the category for a session
*/
get: (sessionID: string): string | undefined => {
return sessionCategoryMap.get(sessionID)
},
/**
* Remove a session from the registry (cleanup)
*/
remove: (sessionID: string): void => {
sessionCategoryMap.delete(sessionID)
},
/**
* Check if a session is registered
*/
has: (sessionID: string): boolean => {
return sessionCategoryMap.has(sessionID)
},
/**
* Get the size of the registry (for debugging)
*/
size: (): number => {
return sessionCategoryMap.size
},
/**
* Clear all entries (use with caution, mainly for testing)
*/
clear: (): void => {
sessionCategoryMap.clear()
},
}