feat(sdk/llm): layered per-model config + caps via optional store interfaces#25
Merged
Conversation
…nterfaces Closes #24. DefaultResolver previously consumed a single ProviderConfigStore and silently dropped any per-model overrides users wrote (e.g. extra.caps on a "model:openai/gpt-4o" row never reached CapsMiddleware). This change introduces two opt-in store extensions plus strong-typed override structs so per-model config rounds-trip end-to-end without losing type safety: - ModelConfigStore.GetModelConfig — per-model {Caps, Extra} overrides that shallow-merge over the provider config and OR-merge with the catalog/provider/extra caps layers. - DefaultModelStore.GetDefaultModel — typed default-model pointer, replacing the magic "__global_default__" provider row. - ProviderConfig.Caps — explicit provider-wide caps field that no longer requires stuffing into the untyped Config map. Caps now compose as an OR across (registry catalog, ProviderConfig.Caps, legacy Config["caps"], ModelConfig.Caps, resolver-wide WithExtraCaps); any layer disabling a capability wins. Backward compatibility: - Stores that only implement ProviderConfigStore (e.g. animus) keep working unchanged — Resolve() never type-asserts mandatorily. - The legacy GlobalDefaultProvider lookup still runs when DefaultModelStore is absent or returns NotFound. - capsFromConfig still reads the legacy Config["caps"] sub-object. - WithModelCaps is renamed to WithExtraCaps; the old name is kept as a thin forwarding alias. Deprecated, scheduled for removal in v0.2.0: - llm.GlobalDefaultProvider constant - llm.WithModelCaps option (use WithExtraCaps) - capsFromConfig / Config["caps"] (use ProviderConfig.Caps or ModelConfig.Caps) Tests cover: - per-model Extra shallow-merge over provider config - ModelConfigStore NotFound vs other-error semantics - DefaultModelStore preferred / falls back to legacy / falls back to WithFallbackModel - 4-layer caps OR merge (catalog × provider × model × resolver-extra) - legacy Config["caps"] still respected - provider-only stores keep working (animus regression) - WithModelCaps deprecation alias still gates caps Made-with: Cursor
…through public API Two cleanups bundled together because they're both follow-ups to the per-model-config refactor (ba229c4): 1. Deprecation audit. Every Deprecated: tag in sdk/ now explicitly states removal in v0.2.0, matching the policy already documented on the resolver/telemetry options: - sdk/kanban/board.go: TaskBoard, NewTaskBoard - sdk/kanban/scheduler.go: (*Scheduler).SetKanban - sdk/graph/compiler/compiler.go: ValidateGraphDef - sdk/llm/factory.go: capsFromConfig wording cleanup sdkx/ has no Deprecated symbols and was unchanged. sdk/workflow/board.go's migrateVarsMessages is intentionally left alone: it is unexported (Go tooling never treats it as a deprecation) and its "v2 migration window" refers to the board-snapshot data format, not the SDK module version. 2. Test API hygiene. Resolver tests no longer construct &defaultResolver{...} directly or reference the deprecated GlobalDefaultProvider constant / WithModelCaps option. They go through DefaultResolver(...) + WithFallbackModel / WithExtraCaps, plus a single test-only helper newResolverWithRegistry that swaps the registry on the resolver returned by DefaultResolver — the only field tests need to override that the public API doesn't expose. Tests that exercise the legacy "__global_default__" magic key are renamed with a LegacyMagicKey_ prefix and a comment pointing at the v0.2.0 removal so they're easy to delete together with the constant. Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #24.
DefaultResolverpreviously consumed a singleProviderConfigStoreand silently dropped any per-model overrides users wrote — e.g.extra.capsstored on amodel:openai/gpt-4orow never reachedCapsMiddleware. This PR adds two opt-in store extensions, one strong-typed override field, and rewritescreateLLMso per-model config rounds-trip end-to-end.New surface
ModelConfigStore.GetModelConfig(ctx, provider, model){Caps, Extra}overrides;errdefs.NotFoundis silently ignored, any other error failsResolve.DefaultModelStore.GetDefaultModel(ctx)"global_default"provider row.ProviderConfig.CapsConfigmap.ModelConfig{Provider, Model, Caps, Extra}ModelConfigStore.DefaultModelRef{Provider, Model}DefaultModelStore.WithExtraCaps(caps)WithModelCaps(made the "additive" semantics explicit).Caps merge order
createLLMnow OR-merges five layers — any layer disabling a capability wins:ProviderRegistry.LookupModelCaps(provider, model)— static catalogProviderConfig.Caps— provider-wide user configcapsFromConfig(merged)— legacyConfig["caps"](deprecated, still honored)ModelConfig.Caps— per-model user configresolver.extraCaps—WithExtraCaps/WithModelCapsModelConfig.Extrais shallow-merged (per-model keys overwrite provider keys) before being handed to the provider factory.Default-model lookup order
Resolve("")now tries:DefaultModelStore.GetDefaultModel(preferred)"global_default"provider row (deprecated)WithFallbackModelBackward compatibility
animus) keep working unchanged —Resolveonly type-asserts the optional interfaces."global_default"lookup still runs whenDefaultModelStoreis absent or returnsNotFound.capsFromConfigstill parses the legacyConfig["caps"]sub-object.WithModelCapsis preserved as a thin alias forwarding toWithExtraCaps.Deprecations (all tagged for removal in v0.2.0)
This PR also audits the rest of
sdk/and ensures everyDeprecated:tag explicitly states removal in v0.2.0:llm.GlobalDefaultProvider— implementDefaultModelStoreinsteadllm.WithModelCaps— useWithExtraCapsllm.capsFromConfig/Config["caps"]— useProviderConfig.Caps/ModelConfig.Capskanban.TaskBoard,kanban.NewTaskBoard— useBoard/NewBoard(*kanban.Scheduler).SetKanban— useWithSchedulercompiler.ValidateGraphDef— calldef.Validate()directlytelemetry.WithLogExporter/WithLogConsole/WithLogMinSeverity(already tagged before this PR)sdkx/has noDeprecated:symbols and is unchanged.Test API hygiene (commit 2)
Resolver tests no longer construct
&defaultResolver{...}directly or reference deprecated symbols. They go throughDefaultResolver(...)+WithFallbackModel/WithExtraCaps, plus a single test-onlynewResolverWithRegistryhelper that swaps the registry on a realDefaultResolver— the only field tests need to override that the public API doesn't expose.Tests covering the legacy magic key are renamed with a
LegacyMagicKey_prefix and a comment pointing at the v0.2.0 removal, so they're easy to delete together with the constant.Test plan
cd sdk && go test ./...— all greencd sdkx && go test ./...— all green (no consumer changes needed; SDKX doesn't touchllm.ProviderConfig/DefaultResolver)cd plugin && go test ./...— all greengo build ./... && go vet ./...per module — cleansdk/llm/resolver_layered_test.gocover:Extrashallow-merge over provider configModelConfigStoreNotFoundvs other-error semanticsDefaultModelStorepreferred / falls back to legacy magic key / falls back toWithFallbackModelConfig["caps"]still respected (deprecation regression)sdk/v0.1.11; cascade PR to bumpsdkx/go.modwill follow automatically.Notes for reviewers
unwrapCapsexists to avoid double-wrappingCapsMiddleware:NewFromConfigalready wraps the instance once with(catalog ⊕ legacy Config["caps"]), and the resolver re-wraps with the full 5-layer composition. WithoutunwrapCapsthe caller would see two stacked wrappers — functionally correct but harder to reason about and slightly less efficient.mergeCapswas changed from(a, b)to variadic(...)to make the 5-layer composition increateLLMread top-to-bottom without nesting.migrateVarsMessagesinsdk/workflow/board.gois left untouched: it's unexported (Go tooling never treats it as a deprecation) and its "v2 migration window" refers to the board-snapshot data format, not the SDK module version.Made with Cursor