P3 internal#718: serve GET /templates selectable provider/model list FROM the registry (PR-A backend; NOT merged) #1977
@@ -95,6 +95,38 @@ type modelSpec struct {
|
||||
Name string `json:"name,omitempty" yaml:"name"`
|
||||
Provider string `json:"provider,omitempty" yaml:"provider"`
|
||||
RequiredEnv []string `json:"required_env,omitempty" yaml:"required_env"`
|
||||
// BillingMode is the billing source the DERIVED provider implies:
|
||||
// "platform_managed" (the closed core-only platform provider; Molecule
|
||||
// owns the upstream key + the bill) or "byok" (any other provider; the
|
||||
// tenant supplies its own key). Set ONLY on registry-served models
|
||||
// (RegistryModels) where DeriveProvider resolved an owning provider;
|
||||
// empty on template-served models. internal#718 P3 — the canvas reads
|
||||
// this to show the billing-mode of the DERIVED provider instead of its
|
||||
// hardcoded billingModeForProvider rule.
|
||||
BillingMode string `json:"billing_mode,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// registryProviderView is the canvas-facing projection of a single registry
|
||||
// Provider entry for a registry-known runtime: the stable name, the dropdown
|
||||
// display label, the auth-env-var NAMES (never values), and the billing mode
|
||||
// the provider implies. Sourced from the provider registry
|
||||
// (internal/providers) so the canvas drops its hardcoded VENDOR_LABELS map
|
||||
// and billingModeForProvider rule (internal#718 P3, retire-list #4/#5).
|
||||
type registryProviderView struct {
|
||||
// Name is the registry provider key (e.g. "anthropic-oauth", "platform").
|
||||
Name string `json:"name"`
|
||||
// DisplayName is the canvas dropdown label (registry Provider.DisplayName).
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
// AuthEnv is the env-var NAMES any one of which satisfies auth for this
|
||||
// provider (registry Provider.AuthEnv). Names only, never secret values.
|
||||
AuthEnv []string `json:"auth_env,omitempty"`
|
||||
// BillingMode is "platform_managed" for the closed platform provider,
|
||||
// "byok" otherwise — keyed off the registry IsPlatform predicate so the
|
||||
// canvas shows the DERIVED provider's billing source.
|
||||
BillingMode string `json:"billing_mode,omitempty"`
|
||||
// Deprecated mirrors the registry's deprecated flag so the canvas can
|
||||
// grey the provider out without breaking saved configs.
|
||||
Deprecated bool `json:"deprecated,omitempty"`
|
||||
}
|
||||
|
||||
// providerRegistryEntry mirrors a row from a template's top-level
|
||||
@@ -162,8 +194,29 @@ type templateSummary struct {
|
||||
// (omitempty); the canvas's existing per-model fallback continues
|
||||
// to work for them.
|
||||
ProviderRegistry []providerRegistryEntry `json:"provider_registry,omitempty"`
|
||||
Skills []string `json:"skills"`
|
||||
SkillCount int `json:"skill_count"`
|
||||
// RegistryBacked is true when this template's runtime is known to the
|
||||
// provider registry (internal/providers runtimes: block) and the
|
||||
// RegistryProviders / RegistryModels fields below were populated from it.
|
||||
// The canvas treats a registry-backed payload as AUTHORITATIVE for the
|
||||
// selectable provider+model list (it drops its prefix-inference fallback)
|
||||
// — "only registered selectable" follows because the canvas can render
|
||||
// no option the registry did not serve. False = the runtime is not in the
|
||||
// registry (federation / external / mock); the canvas keeps using the
|
||||
// template-served Models/Providers + its heuristic. internal#718 P3.
|
||||
RegistryBacked bool `json:"registry_backed,omitempty"`
|
||||
// RegistryProviders is the runtime's NATIVE provider set from the
|
||||
// registry (ProvidersForRuntime), each with its display label, auth-env
|
||||
// names, and billing mode. Empty when !RegistryBacked. This is the SSOT
|
||||
// the canvas Provider dropdown consumes instead of VENDOR_LABELS.
|
||||
RegistryProviders []registryProviderView `json:"registry_providers,omitempty"`
|
||||
// RegistryModels is the runtime's NATIVE model set from the registry
|
||||
// (ModelsForRuntime), each annotated with its DERIVED provider and the
|
||||
// billing mode that provider implies. Empty when !RegistryBacked. This is
|
||||
// the SSOT the canvas Model dropdown consumes — a template can no longer
|
||||
// surface a model the registry does not list for the runtime.
|
||||
RegistryModels []modelSpec `json:"registry_models,omitempty"`
|
||||
Skills []string `json:"skills"`
|
||||
SkillCount int `json:"skill_count"`
|
||||
// ProvisionTimeoutSeconds lets a slow runtime declare its expected
|
||||
// cold-boot duration in its template manifest. Canvas's
|
||||
// ProvisioningTimeout banner respects this per-workspace via the
|
||||
@@ -243,9 +296,13 @@ func (h *TemplatesHandler) List(c *gin.Context) {
|
||||
log.Printf("templates list: skip %s: yaml.Unmarshal: %v", id, err)
|
||||
return
|
||||
}
|
||||
// normalizedRuntime strips the "-default" vanilla-variant suffix
|
||||
// (claude-code-default → claude-code). Hoisted out of the
|
||||
// known-runtime guard so the registry enrichment below can key off
|
||||
// the same normalised name the guard validated.
|
||||
normalizedRuntime := strings.TrimSuffix(strings.TrimSpace(raw.Runtime), "-default")
|
||||
if raw.Runtime != "" {
|
||||
runtime := strings.TrimSuffix(strings.TrimSpace(raw.Runtime), "-default")
|
||||
if _, ok := knownRuntimes[runtime]; !ok {
|
||||
if _, ok := knownRuntimes[normalizedRuntime]; !ok {
|
||||
log.Printf("templates list: skip %s: unsupported runtime %q", id, raw.Runtime)
|
||||
return
|
||||
}
|
||||
@@ -262,7 +319,7 @@ func (h *TemplatesHandler) List(c *gin.Context) {
|
||||
tier = h.wh.DefaultTier()
|
||||
}
|
||||
|
||||
templates = append(templates, templateSummary{
|
||||
summary := templateSummary{
|
||||
ID: id,
|
||||
Name: raw.Name,
|
||||
Description: raw.Description,
|
||||
@@ -277,7 +334,17 @@ func (h *TemplatesHandler) List(c *gin.Context) {
|
||||
Skills: raw.Skills,
|
||||
SkillCount: len(raw.Skills),
|
||||
ProvisionTimeoutSeconds: raw.RuntimeConfig.ProvisionTimeoutSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
// internal#718 P3: serve the SELECTABLE provider/model list from
|
||||
// the provider registry for a registry-known runtime. Additive —
|
||||
// the template-served Models/Providers above stay for non-registry
|
||||
// runtimes + older canvases; this adds the authoritative
|
||||
// registry_backed/registry_providers/registry_models block the
|
||||
// current canvas prefers. Fail-open for unknown runtimes.
|
||||
enrichFromRegistry(&summary, normalizedRuntime)
|
||||
|
||||
templates = append(templates, summary)
|
||||
})
|
||||
}
|
||||
walk(h.cacheDir)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package handlers
|
||||
|
||||
// templates_registry.go — internal#718 P3: serve the GET /templates selectable
|
||||
// provider/model list FROM the provider registry (workspace-server/internal/
|
||||
// providers) instead of from each template's hand-authored config.yaml
|
||||
// `providers:` / `runtime_config.models` block.
|
||||
//
|
||||
// The registry (P2-A synced copy of the canonical CP providers.yaml) is the
|
||||
// SSOT for "which providers + models does runtime R natively support" and
|
||||
// "which derived provider owns model M" (DeriveProvider) and "is that provider
|
||||
// the closed platform set" (IsPlatform). This file projects that into the
|
||||
// templates payload's registry_backed / registry_providers / registry_models
|
||||
// fields so the canvas can drop its hardcoded VENDOR_LABELS /
|
||||
// billingModeForProvider vocabularies (retire-list #4/#5) and physically can't
|
||||
// render an option the registry didn't serve.
|
||||
//
|
||||
// Federation-ready, fail-OPEN: a runtime ABSENT from the registry's runtimes:
|
||||
// block (external / mock / kimi / a future third-party runtime) yields
|
||||
// RegistryBacked=false and an empty registry block — the template's own fields
|
||||
// stay authoritative. No behavior change for non-registry runtimes.
|
||||
//
|
||||
// NOTE: this reuses the package-level providerRegistry() accessor +
|
||||
// LLMBillingModePlatformManaged / LLMBillingModeBYOK constants from
|
||||
// llm_billing_mode.go (added by P2-B, internal#718 #1972, now on main) — both
|
||||
// the billing-derivation and this templates projection wrap the same
|
||||
// providers.LoadManifest() SSOT and the same platform_managed/byok wire
|
||||
// strings, so there is one accessor + one constant set for the package.
|
||||
|
||||
import (
|
||||
"git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/providers"
|
||||
)
|
||||
|
||||
// billingModeForRegistryProvider maps a registry Provider to the billing mode
|
||||
// it implies: platform_managed for the closed core-only platform provider,
|
||||
// byok for everything else. Keyed off the registry IsPlatform predicate —
|
||||
// the same one billing/credential emission (llm_billing_mode.go) keys off the
|
||||
// DERIVED provider — so the canvas shows the true billing source of the
|
||||
// resolved provider. Returns the same LLMBillingMode* wire strings the Config
|
||||
// tab's billing-mode switch sends.
|
||||
func billingModeForRegistryProvider(p providers.Provider) string {
|
||||
if p.IsPlatform() {
|
||||
return LLMBillingModePlatformManaged
|
||||
}
|
||||
return LLMBillingModeBYOK
|
||||
}
|
||||
|
||||
// enrichFromRegistry populates the registry-served fields on a templateSummary
|
||||
// when its runtime is known to the provider registry. It is a no-op (leaves
|
||||
// RegistryBacked=false and the registry slices nil) for a runtime the registry
|
||||
// does not know — the federation/fail-open path.
|
||||
//
|
||||
// runtime is the template's already-normalised runtime string (the caller
|
||||
// strips the "-default" suffix before calling, matching List's existing
|
||||
// knownRuntimes check).
|
||||
func enrichFromRegistry(summary *templateSummary, runtime string) {
|
||||
m, err := providerRegistry()
|
||||
if err != nil || m == nil {
|
||||
return // fail open — registry load defect; keep template-served fields.
|
||||
}
|
||||
|
||||
provs, err := m.ProvidersForRuntime(runtime)
|
||||
if err != nil {
|
||||
// Runtime not in the registry runtimes: block (external / mock / kimi
|
||||
// / future third-party). Fail open: the template's own fields stay
|
||||
// authoritative; no registry annotation.
|
||||
return
|
||||
}
|
||||
|
||||
// registry_providers — the runtime's native provider set, in registry
|
||||
// declared order, projected to the canvas-facing view.
|
||||
views := make([]registryProviderView, 0, len(provs))
|
||||
for _, p := range provs {
|
||||
views = append(views, registryProviderView{
|
||||
Name: p.Name,
|
||||
DisplayName: p.DisplayName,
|
||||
AuthEnv: p.AuthEnv,
|
||||
BillingMode: billingModeForRegistryProvider(p),
|
||||
Deprecated: p.Deprecated,
|
||||
})
|
||||
}
|
||||
|
||||
// registry_models — the runtime's native model ids, each annotated with
|
||||
// the DERIVED owning provider + the billing mode it implies. DeriveProvider
|
||||
// is the SSOT for model→provider; we pass nil availableAuthEnv because a
|
||||
// template manifest has no per-workspace auth env, and the registry's
|
||||
// exact-id mapping resolves every native model id unambiguously (the
|
||||
// claude-code kimi split is by exact id, not a shared prefix).
|
||||
models, err := m.ModelsForRuntime(runtime)
|
||||
if err != nil {
|
||||
// ProvidersForRuntime succeeded but ModelsForRuntime did not — should
|
||||
// be impossible (both gate on the same Runtimes entry), but fail open
|
||||
// rather than serve a half-populated block.
|
||||
return
|
||||
}
|
||||
regModels := make([]modelSpec, 0, len(models))
|
||||
for _, id := range models {
|
||||
ms := modelSpec{ID: id}
|
||||
if derived, derr := m.DeriveProvider(runtime, id, nil); derr == nil {
|
||||
ms.Provider = derived.Name
|
||||
ms.BillingMode = billingModeForRegistryProvider(derived)
|
||||
}
|
||||
// If DeriveProvider errors (ambiguous/overlap — a manifest defect the
|
||||
// loader's tests pin against), still serve the id without a provider
|
||||
// annotation rather than dropping it; the canvas treats an
|
||||
// un-annotated registry model as selectable-but-unlabelled.
|
||||
regModels = append(regModels, ms)
|
||||
}
|
||||
|
||||
summary.RegistryBacked = true
|
||||
summary.RegistryProviders = views
|
||||
summary.RegistryModels = regModels
|
||||
}
|
||||
@@ -1329,3 +1329,228 @@ func TestCWE78_DeleteFile_TraversalVariants(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// internal#718 P3 — GET /templates serves the selectable provider/model list
|
||||
// FROM the provider registry (workspace-server/internal/providers), not from
|
||||
// each template's hand-authored config.yaml. Additive: the registry-served
|
||||
// fields (registry_backed / registry_providers / registry_models) ride
|
||||
// ALONGSIDE the existing template-served fields so non-registry runtimes and
|
||||
// older canvases keep working. The canvas (PR-B) prefers the registry block;
|
||||
// "only registered selectable" follows because the registry block is the
|
||||
// authoritative list for a registry-known runtime.
|
||||
// ============================================================================
|
||||
|
||||
// TestTemplatesList_RegistryServesSelectableModels pins the core P3 contract:
|
||||
// for a runtime the provider registry knows (claude-code), /templates serves
|
||||
// the registry's NATIVE model ids — regardless of what the template's
|
||||
// config.yaml runtime_config.models happens to list. A template author can no
|
||||
// longer surface an unregistered model into the canvas dropdown.
|
||||
func TestTemplatesList_RegistryServesSelectableModels(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmplDir := filepath.Join(tmpDir, "claude-code-default")
|
||||
if err := os.MkdirAll(tmplDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
// Deliberately list a BOGUS model the registry does not know. The
|
||||
// registry-served list must NOT contain it.
|
||||
configYaml := `name: Claude Code
|
||||
runtime: claude-code
|
||||
runtime_config:
|
||||
model: claude-sonnet-4-6
|
||||
models:
|
||||
- id: totally-made-up-model
|
||||
name: Not In Registry
|
||||
skills: []
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tmplDir, "config.yaml"), []byte(configYaml), 0644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
handler := NewTemplatesHandler(tmpDir, nil, nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/templates", nil)
|
||||
handler.List(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var resp []templateSummary
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if len(resp) != 1 {
|
||||
t.Fatalf("expected 1 template, got %d", len(resp))
|
||||
}
|
||||
got := resp[0]
|
||||
|
||||
if !got.RegistryBacked {
|
||||
t.Fatalf("claude-code is a registry-known runtime; RegistryBacked must be true")
|
||||
}
|
||||
|
||||
// The registry-served model set must be the claude-code native set
|
||||
// (anthropic-oauth: sonnet/opus/haiku, anthropic-api: claude-*-4-*,
|
||||
// kimi-coding: kimi-*, minimax: MiniMax-*, platform: vendor/model ids).
|
||||
// It must NOT contain the template's bogus id.
|
||||
regModelIDs := map[string]bool{}
|
||||
for _, m := range got.RegistryModels {
|
||||
regModelIDs[m.ID] = true
|
||||
}
|
||||
if regModelIDs["totally-made-up-model"] {
|
||||
t.Errorf("RegistryModels leaked the template's unregistered model id")
|
||||
}
|
||||
for _, want := range []string{"sonnet", "opus", "claude-opus-4-7", "anthropic/claude-opus-4-7"} {
|
||||
if !regModelIDs[want] {
|
||||
t.Errorf("RegistryModels missing native model %q; got %v", want, regModelIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplatesList_RegistryAnnotatesDerivedProviderAndBilling pins that each
|
||||
// registry-served model carries its DERIVED provider name + a billing_mode
|
||||
// reflecting whether that derived provider is the closed platform set
|
||||
// (platform_managed) or BYOK (byok). This is what the canvas Config tab reads
|
||||
// to show the billing-mode of the DERIVED provider (folds in #1931 intent),
|
||||
// instead of its hardcoded billingModeForProvider rule.
|
||||
func TestTemplatesList_RegistryAnnotatesDerivedProviderAndBilling(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmplDir := filepath.Join(tmpDir, "claude-code-default")
|
||||
if err := os.MkdirAll(tmplDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
configYaml := `name: Claude Code
|
||||
runtime: claude-code
|
||||
runtime_config:
|
||||
model: claude-sonnet-4-6
|
||||
skills: []
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tmplDir, "config.yaml"), []byte(configYaml), 0644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
handler := NewTemplatesHandler(tmpDir, nil, nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/templates", nil)
|
||||
handler.List(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var resp []templateSummary
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
got := resp[0]
|
||||
|
||||
billByModel := map[string]string{}
|
||||
provByModel := map[string]string{}
|
||||
for _, m := range got.RegistryModels {
|
||||
billByModel[m.ID] = m.BillingMode
|
||||
provByModel[m.ID] = m.Provider
|
||||
}
|
||||
|
||||
// A BYOK API model derives to anthropic-api → byok.
|
||||
if provByModel["claude-opus-4-7"] != "anthropic-api" {
|
||||
t.Errorf("claude-opus-4-7 derived provider: want anthropic-api, got %q", provByModel["claude-opus-4-7"])
|
||||
}
|
||||
if billByModel["claude-opus-4-7"] != "byok" {
|
||||
t.Errorf("claude-opus-4-7 billing_mode: want byok, got %q", billByModel["claude-opus-4-7"])
|
||||
}
|
||||
// A platform-namespaced model derives to the closed platform provider →
|
||||
// platform_managed.
|
||||
if provByModel["anthropic/claude-opus-4-7"] != "platform" {
|
||||
t.Errorf("anthropic/claude-opus-4-7 derived provider: want platform, got %q", provByModel["anthropic/claude-opus-4-7"])
|
||||
}
|
||||
if billByModel["anthropic/claude-opus-4-7"] != "platform_managed" {
|
||||
t.Errorf("anthropic/claude-opus-4-7 billing_mode: want platform_managed, got %q", billByModel["anthropic/claude-opus-4-7"])
|
||||
}
|
||||
|
||||
// registry_providers carries the provider display_name + auth_env +
|
||||
// billing_mode for the dropdown labels — sourced from the registry, not
|
||||
// the canvas VENDOR_LABELS map.
|
||||
byName := map[string]registryProviderView{}
|
||||
for _, p := range got.RegistryProviders {
|
||||
byName[p.Name] = p
|
||||
}
|
||||
oauth, ok := byName["anthropic-oauth"]
|
||||
if !ok {
|
||||
t.Fatalf("registry_providers missing anthropic-oauth; got %v", byName)
|
||||
}
|
||||
if oauth.DisplayName != "Claude Code subscription" {
|
||||
t.Errorf("anthropic-oauth display_name: want %q, got %q", "Claude Code subscription", oauth.DisplayName)
|
||||
}
|
||||
if oauth.BillingMode != "byok" {
|
||||
t.Errorf("anthropic-oauth billing_mode: want byok, got %q", oauth.BillingMode)
|
||||
}
|
||||
if len(oauth.AuthEnv) != 1 || oauth.AuthEnv[0] != "CLAUDE_CODE_OAUTH_TOKEN" {
|
||||
t.Errorf("anthropic-oauth auth_env: want [CLAUDE_CODE_OAUTH_TOKEN], got %v", oauth.AuthEnv)
|
||||
}
|
||||
plat, ok := byName["platform"]
|
||||
if !ok || plat.BillingMode != "platform_managed" {
|
||||
t.Errorf("platform provider billing_mode: want platform_managed, got %+v", plat)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplatesList_NonRegistryRuntimeFallsOpenToTemplate pins federation-
|
||||
// readiness: for a runtime the registry does NOT know (a hypothetical
|
||||
// third-party / external-like runtime), /templates does NOT set
|
||||
// RegistryBacked and does NOT synthesize a registry block — the template's
|
||||
// own config.yaml fields remain the source, unchanged. No behavior change for
|
||||
// non-registry runtimes.
|
||||
func TestTemplatesList_NonRegistryRuntimeFallsOpenToTemplate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmplDir := filepath.Join(tmpDir, "byo-runtime")
|
||||
if err := os.MkdirAll(tmplDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
// "mock" is a known runtime to the manifest allowlist (so List doesn't
|
||||
// skip it) but is NOT in the provider registry's runtimes: block.
|
||||
configYaml := `name: Mock Runtime
|
||||
runtime: mock
|
||||
runtime_config:
|
||||
model: canned-reply
|
||||
providers: [some-byo-provider]
|
||||
models:
|
||||
- id: canned-reply
|
||||
name: Canned Reply
|
||||
skills: []
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tmplDir, "config.yaml"), []byte(configYaml), 0644); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
|
||||
handler := NewTemplatesHandler(tmpDir, nil, nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/templates", nil)
|
||||
handler.List(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var resp []templateSummary
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if len(resp) != 1 {
|
||||
t.Fatalf("expected 1 template, got %d", len(resp))
|
||||
}
|
||||
got := resp[0]
|
||||
|
||||
if got.RegistryBacked {
|
||||
t.Errorf("mock is NOT a registry runtime; RegistryBacked must be false")
|
||||
}
|
||||
if len(got.RegistryModels) != 0 || len(got.RegistryProviders) != 0 {
|
||||
t.Errorf("non-registry runtime must not synthesize a registry block; got models=%v providers=%v",
|
||||
got.RegistryModels, got.RegistryProviders)
|
||||
}
|
||||
// Template-served fields untouched.
|
||||
if len(got.Models) != 1 || got.Models[0].ID != "canned-reply" {
|
||||
t.Errorf("template Models unchanged: got %+v", got.Models)
|
||||
}
|
||||
if !reflect.DeepEqual(got.Providers, []string{"some-byo-provider"}) {
|
||||
t.Errorf("template Providers unchanged: got %v", got.Providers)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user