Skip to content

feat(strategies/cost): add CapabilityAggregator to prevent opaque-cost misrouting#241

Open
gr3enarr0w wants to merge 6 commits into
ferro-labs:mainfrom
gr3enarr0w:feat/capability-aggregator
Open

feat(strategies/cost): add CapabilityAggregator to prevent opaque-cost misrouting#241
gr3enarr0w wants to merge 6 commits into
ferro-labs:mainfrom
gr3enarr0w:feat/capability-aggregator

Conversation

@gr3enarr0w

@gr3enarr0w gr3enarr0w commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Closes #244

Problem

CostOptimized routes to the cheapest provider by looking up prices in the model catalog. For aggregator providers like OpenRouter (already in the codebase) and NanoGPT (#240), SupportsModel() returns true for every model. If their catalog price is lower than a direct provider's, CostOptimized picks the aggregator — but the catalog price does not include the aggregator's own platform markup. The gateway silently routes through an aggregator with unknown real cost.

There's also a bug under unpricedStrategy: allow: forcing hasPrice=false on an aggregator doesn't help because allow mode bypasses the !hasPrice guard, so the aggregator wins at costUSD=0.0.

Solution

Add CapabilityAggregator = "aggregator" to the existing CapabilityXxx constants and exclude aggregator-tagged providers from cost ranking in both the strategy and the streaming path.

Changes

providers/factory.go

  • Add CapabilityAggregator = "aggregator" constant

providers/providers_list.go

internal/strategies/costoptimized.go

  • Add isAggregator bool field to priced candidate struct
  • Add WithAggregatorPredicate(fn func(virtualKey string) bool) *CostOptimized builder
  • selectCostOptimizedCandidate skips aggregators unconditionally — the critical fix for the allow-mode bypass
  • Positional fallback prefers first non-aggregator

gateway.go

  • streamingCostOrderLocked (parallel streaming cost path) now also excludes aggregators
  • Wire WithAggregatorPredicate when constructing CostOptimized

providers/nanogpt/nanogpt.go

  • Replace hallucinated model names in SupportedModels() with real ones

internal/strategies/costoptimized_test.go

  • TestCostOptimized_AggregatorExcludedUnderAllowMode
  • TestCostOptimized_AggregatorPositionalFallbackSkippedWhenDirectExists

Behavior change

unpricedStrategy Before After
fallback (default) Aggregator could win if catalog priced it lower Excluded from ranking; used only when no direct provider supports the model
skip Aggregator skipped (only because hasPrice=false) Same, now explicit
allow Bug: aggregator always won at costUSD=0.0 Excluded regardless of mode

No breaking changes for users without aggregators configured.

Unblocks

Test plan

  • go test ./internal/strategies/... -run TestCostOptimized
  • go test ./providers/...
  • go test ./...

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved cost-optimized routing to prefer direct providers over aggregators when pricing is available.
    • Aggregators remain available as fallbacks when no direct provider can serve a request.
    • OpenRouter is now recognized as an aggregator provider.
  • Bug Fixes

    • Updated Ollama Cloud connections to use the current API endpoint.
    • Corrected Ollama Cloud configuration variable names in the Compose example.

Clark Everson and others added 4 commits June 27, 2026 20:11
Adds providers/nanogpt — an aggregator integration with nano-gpt.com that
proxies requests to 20+ upstream models (Anthropic, OpenAI, xAI, DeepSeek,
Moonshot, Google, Mistral, etc.). Implements core.Provider, core.StreamProvider,
core.ProxiableProvider, and core.DiscoveryProvider via the existing
internal/openaicompat helpers.

Splits out the NanoGPT portion of PR ferro-labs#230 as requested by @MitulShah1.
Parked alongside ferro-labs#226 pending aggregator design decision.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… name

- Change defaultBaseURL from https://ollama.com (website) to
  https://api.ollama.com (actual Ollama Cloud API endpoint)
- Rename env var gate from OLLAMA_API_KEY to OLLAMA_CLOUD_API_KEY to
  avoid ambiguity with the local Ollama provider (OLLAMA_HOST)
- Update docker-compose.yml comment to reflect both corrections

The Ollama Cloud provider was introduced without these fixes, which would
cause default-configured instances to point at the marketing website
rather than the inference API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gators from cost-optimized routing

Aggregator providers (OpenRouter, NanoGPT) have SupportsModel()=true for
all models and opaque upstream costs that include platform markups. When
CostOptimized uses catalog prices for these providers, it silently
misroutes — the catalog entry doesn't reflect the aggregator's fee.

Adds CapabilityAggregator = "aggregator" to the capability constants and
tags OpenRouter and NanoGPT. In CostOptimized.Execute(), any provider
tagged as an aggregator is forced to hasPrice=false regardless of catalog
entries, making it fall through to unpricedStrategy (default: "fallback"
— only used when no direct priced provider is available).

Wired via WithAggregatorPredicate in gateway.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nd NanoGPT model list

Three bugs found in code review:

1. allow-mode bypass: isAggregator forced costUSD=0.0, but under
   unpricedStrategy=allow the hasPrice guard is skipped, making
   the aggregator win every cost comparison at $0.00. Fix: add
   isAggregator field to priced struct; skip aggregators in ranking
   loop unconditionally (mode-independent). Also fix positional
   fallback (candidates[0]) to prefer the first non-aggregator target.

2. Streaming gap: streamingCostOrderLocked built candidates from raw
   catalog prices with no aggregator check. All three switch cases
   now filter candidates where isAggregator=true. Uses
   ProviderHasCapability directly (mirrors the strategy predicate).

3. NanoGPT hallucinated models: SupportedModels() contained
   fabricated version numbers (gpt-5.5, claude-opus-4.7, grok-4.20,
   gemini-3.5-flash, etc.). Replaced with real current model names.
   Discovery via DiscoverModels() remains the authoritative source.

Adds tests:
- TestCostOptimized_AggregatorExcludedUnderAllowMode
- TestCostOptimized_AggregatorPositionalFallbackSkippedWhenDirectExists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ❌

244 - Partially compliant

Compliant requirements:

  • Added CapabilityAggregator constant.
  • Tagged OpenRouter and NanoGPT as aggregators in providers/providers_list.go.
  • Added WithAggregatorPredicate, forced aggregator candidates to unpriced treatment, and skipped them in selectCostOptimizedCandidate.
  • Added positional fallback that prefers the first non-aggregator candidate.
  • Excluded aggregators from streaming cost ranking in gateway.go.
  • Wired WithAggregatorPredicate in gateway.go getStrategy.

Non-compliant requirements:

  • Requesty is not tagged with CapabilityAggregator because the Requesty provider is not present in this diff.
  • NanoGPT SupportedModels() does not include deepseek/deepseek-v4-pro, which the new unit test expects.

Requires further human verification:

  • Confirm the real NanoGPT model identifiers in SupportedModels() (e.g., whether deepseek/deepseek-v4-pro should be present).
  • Verify whether the Requesty provider already exists in the base branch and needs the aggregator capability retroactively.

240 - Partially compliant

Compliant requirements:

  • Added providers/nanogpt/nanogpt.go implementing the required interfaces.
  • Added env var mappings NANOGPT_API_KEY / NANOGPT_BASE_URL.
  • Added NameNanoGPT constant and AllProviderNames() entry.
  • Registered NanoGPT in providers/providers_list.go.
  • Extended stability tests.
  • Added unit tests for New, SupportsModel, Models, AuthHeaders, and mock streaming/non-streaming completions.

Non-compliant requirements:

  • SupportedModels() does not match the test expectation for deepseek/deepseek-v4-pro; TestNanoGPTProvider_SupportedModels will fail.

Requires further human verification:

  • Validate the actual NanoGPT model identifiers in SupportedModels() against the live API.

226 - Not compliant

Non-compliant requirements:

  • Requesty provider implementation files are not added.
  • Requesty is not registered in providers/providers_list.go.
  • NameRequesty constant and stability test entry are not present.
  • Config examples are not updated for Requesty.

Requires further human verification:

  • Verify whether the Requesty provider was already merged separately in the base branch; if so, it still needs the CapabilityAggregator tag.
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Test Failure

SupportedModels() omits deepseek/deepseek-v4-pro, but providers/nanogpt/nanogpt_test.go asserts it is present, so go test ./providers/nanogpt will fail.

func (p *Provider) SupportedModels() []string {
	return []string{
		"anthropic/claude-sonnet-4-5",
		"anthropic/claude-haiku-3-5",
		"openai/gpt-4o",
		"openai/gpt-4o-mini",
		"x-ai/grok-2",
		"deepseek/deepseek-r1",
		"google/gemini-2-5-flash",
		"google/gemini-2-5-pro",
		"mistral/mistral-large-latest",
	}
}
Streaming Fallback Inconsistency

In streamingCostOrderLocked, aggregators are excluded from the ranked slice but the unranked remainder is appended in original target order. When no priced direct provider exists and an aggregator target is configured before an unpriced direct provider, the streaming cost order will place the aggregator first, contradicting the non-streaming fallback rule that prefers the first non-aggregator.

ranked := make([]streamingCostCandidate, 0, len(candidates))
switch g.config.Strategy.UnpricedStrategy {
case unpricedStrategyAllow:
	for _, candidate := range candidates {
		// Aggregators are excluded from cost ranking in all modes; they fall
		// to the unranked remainder returned after ranked keys.
		if candidate.modelFound && !candidate.isAggregator {
			ranked = append(ranked, candidate)
		}
	}
case unpricedStrategySkip:
	for _, candidate := range candidates {
		if candidate.modelFound && candidate.hasPrice && !candidate.isAggregator {
			ranked = append(ranked, candidate)
		}
	}
default:
	for _, candidate := range candidates {
		if candidate.modelFound && candidate.hasPrice && !candidate.isAggregator {
			ranked = append(ranked, candidate)
		}
	}

@MitulShah1 MitulShah1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice iteration on this @gr3enarr0w — the fix is in good shape now. Skipping aggregators before the unpriced/allow check (costoptimized.go:161) closes the bypass in all modes, both the sync and streaming cost paths are covered, and tagging both OpenRouter and NanoGPT with CapabilityAggregator is the right structural move. TestCostOptimized_AggregatorExcludedWhenPricedDirectProviderExists is a genuinely good test.

A few things before I'd merge:

  1. The allow-mode test doesn't actually exercise the bug it namesTestCostOptimized_AggregatorExcludedUnderAllowMode uses buildCatalog(), which only has cheap/+expensive/ entries (no agg//direct/), so both providers resolve unpriced and the headline scenario — a priced aggregator that looks cheapest under allow — isn't covered. Detail inline.
  2. Scope creep — this is a cost-routing change, but it also adds a brand-new NanoGPT provider and the OLLAMA_API_KEY→OLLAMA_CLOUD_API_KEY rename (which also lives in #243 and will conflict). Please split those out so this PR is just the aggregator fix + tagging.
  3. Rationale wording — the "aggregator's own platform markup (OpenRouter +5.5%)" justification isn't accurate: OpenRouter passes token pricing through with no per-token markup; the 5.5% is a one-time credit-purchase fee. The behavior is still right (a single static catalog price can't be trusted for an aggregator that routes dynamically), but let's reword the reason.
  4. Please get Go CI running (only Snyk reported) — this adds a package and touches the stability matrix.

One design question I'll also raise on the issue: since OpenRouter really does pass prices through, should aggregator exclusion be a configurable policy (like unpricedStrategy) rather than a hard default? Not a blocker, just worth deciding deliberately.

agg := &mockProvider{name: "agg", models: []string{"gpt-4o"}, resp: &providers.Response{ID: "agg"}}
direct := &mockProvider{name: "direct", models: []string{"gpt-4o"}, resp: &providers.Response{ID: "direct"}}

catalog := buildCatalog() // direct has pricing, agg has pricing (aggregator markup ignored)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't accurate, and it hides a coverage gap: buildCatalog() only contains cheap/gpt-4o and expensive/gpt-4o — there's no agg/ or direct/ entry. So both agg and direct resolve to ModelFound=false (unpriced), and the test passes via the unpriced-at-$0 path + target ordering, not the scenario it claims. The actual bug from the issue — a priced aggregator that appears cheapest and would win under allow — still isn't exercised anywhere (the WhenPricedDirectProviderExists test covers the priced case but in default mode, not allow).

Suggest an inline catalog here where agg/gpt-4o is priced cheapest (e.g. 0.01) and direct/gpt-4o higher (e.g. 5.0), in allow mode, asserting direct wins. That version would fail if the isAggregator skip on line 161 were removed — which is what makes it a real regression test. Same false-fixture note applies to the comment on line 465.

Comment thread internal/strategies/costoptimized.go Outdated
})
}
// Aggregator providers are never selected by cost ranking: their catalog
// prices do not include the aggregator's own platform markup. They are

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small accuracy fix on the rationale: OpenRouter (and most aggregators) pass underlying token pricing through with no per-token markup — the 5.5% people cite is a one-time credit-purchase fee, not a per-request surcharge. So "prices do not include the aggregator's own platform markup" isn't quite right. The real justification for excluding them is sound though: an aggregator routes dynamically across sub-providers, so a single static catalog price doesn't reliably reflect the effective per-request cost. Suggest rewording to that.

Comment thread providers/nanogpt/nanogpt.go Outdated
@@ -0,0 +1,113 @@
// Package nanogpt provides a client for the NanoGPT API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This net-new NanoGPT provider (and its test) is unrelated to the cost-routing fix — can it go in its own PR? It'd make this one a clean, reviewable "aggregator exclusion" change. (The provider itself looks fine and is correctly tagged CapabilityAggregator in providers_list.go, which is good — just want it decoupled.)

Comment thread providers/providers_list.go Outdated
Capabilities: []string{CapabilityChat, CapabilityStream, CapabilityDiscovery},
EnvMappings: []EnvMapping{
{CfgKeyAPIKey, "OLLAMA_API_KEY", true},
{CfgKeyAPIKey, "OLLAMA_CLOUD_API_KEY", true},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This OLLAMA_API_KEY → OLLAMA_CLOUD_API_KEY rename is the same breaking change that's also in #243 — having it in two open PRs will conflict, and either way it's a breaking config change for existing deployments (OLLAMA_API_KEY is shipped and is Ollama's own convention). Please drop it from this PR; let's settle the env-var question on #243.

Clark Everson added 2 commits July 14, 2026 20:46
…ord rationale

- Split the net-new NanoGPT provider and the OLLAMA_API_KEY->OLLAMA_CLOUD_API_KEY
  rename back out of this PR: NanoGPT already ships separately in ferro-labs#240, and the
  rename duplicates/conflicts with ferro-labs#243's already-merged (as ferro-labs#328) direction.
  This PR is now scoped to just the aggregator cost-ranking exclusion + tagging
  OpenRouter with CapabilityAggregator.
- Fix TestCostOptimized_AggregatorExcludedUnderAllowMode: it used buildCatalog(),
  which has no agg/ or direct/ entries, so both candidates resolved unpriced and
  the test never exercised its own premise (a *priced* aggregator that looks
  cheapest under allow mode). Gave it a catalog where the aggregator carries a
  real, low price so the exclusion path is actually exercised.
- Reworded the aggregator-exclusion rationale throughout: the reason isn't a
  hidden "platform markup" (OpenRouter passes token pricing through with no
  per-token markup - the often-cited 5.5% is a one-time credit-purchase fee,
  not a per-request charge). The real reason is that an aggregator routes each
  request to whichever underlying provider/model it picks at call time, so a
  single static catalog price can't represent the actual per-request cost.

Note: CI has never run on this branch at all (no Actions runs recorded, not
even a pending one) - this needs a maintainer to approve the first workflow
run for this fork branch before Go tests/lint will execute.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Cost-optimized routing now recognizes aggregator providers, excludes them from price ranking, and uses them as fallback candidates. OpenRouter is marked as an aggregator, while Ollama Cloud defaults use updated API endpoint and authentication configuration.

Changes

Aggregator-aware routing

Layer / File(s) Summary
Capability and cost strategy behavior
providers/factory.go, providers/providers_list.go, internal/strategies/costoptimized.go, internal/strategies/costoptimized_test.go
Adds CapabilityAggregator, marks OpenRouter accordingly, excludes aggregators from cost ranking, preserves fallback behavior, and tests priced, unpriced, fallback, and nil-predicate cases.
Gateway cost-routing integration
gateway.go
Configures cost-optimized routing with the aggregator predicate and excludes aggregators from ranked streaming candidates.

Ollama Cloud endpoint configuration

Layer / File(s) Summary
Ollama Cloud API configuration
providers/ollama_cloud/ollama_cloud.go, docker-compose.yml
Updates the default Ollama Cloud base URL and commented cloud authentication variable names.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: mitulshah1

Sequence Diagram(s)

sequenceDiagram
  participant Gateway
  participant CostOptimized
  participant ProviderRegistry
  Gateway->>CostOptimized: Execute cost-optimized request
  CostOptimized->>ProviderRegistry: Check aggregator capability
  CostOptimized-->>Gateway: Return direct provider or aggregator fallback
Loading

Poem

A rabbit hops through ranked arrays,
Keeps cloudy costs from tricky snares.
Direct providers lead the way,
Aggregators wait for fallback day.
Ollama’s cloud endpoint shines bright!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Unrelated docker-compose and Ollama Cloud default changes were included alongside the aggregator-routing fix. Move the docker-compose and Ollama Cloud edits into a separate PR unless they are required for this issue.
Linked Issues check ❓ Inconclusive The cost-routing fix is mostly implemented, but the summary can't confirm all aggregator providers are tagged and fallback ordering is fully safe. Verify/tag every aggregator provider from the issue and confirm streaming and fallback logic never prefers an aggregator over a direct provider.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main change to cost routing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/capability-aggregator

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from MitulShah1 July 15, 2026 01:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gateway.go (1)

1714-1732: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix streaming fallback order to deprioritize aggregators.

Currently, when ranked is empty or when unranked candidates are appended to keys, the loop preserves the original configured targets order. This means an aggregator could precede an unpriced direct provider if placed higher in the YAML list, violating the PR objective which states aggregators should only be used when no direct provider supports the model.

Reorder the unranked concatenation loops to append direct unpriced providers before aggregators, maintaining parity with the non-streaming positional fallback logic.

♻️ Proposed fix
-	if len(ranked) == 0 {
-		if g.config.Strategy.UnpricedStrategy == unpricedStrategySkip {
-			return nil, fmt.Errorf("no priced provider supports model %s", req.Model)
-		}
-		return targetKeys(targets), nil
-	}
+	if len(ranked) == 0 && g.config.Strategy.UnpricedStrategy == unpricedStrategySkip {
+		return nil, fmt.Errorf("no priced provider supports model %s", req.Model)
+	}

	sort.SliceStable(ranked, func(i, j int) bool {
		return ranked[i].costUSD < ranked[j].costUSD
	})

	keys := make([]string, 0, len(targets))
	for _, candidate := range ranked {
		keys = appendUniqueKey(keys, candidate.key)
	}
-	for _, candidate := range candidates {
-		keys = appendUniqueKey(keys, candidate.key)
-	}
+	// Positional fallback for unranked candidates: prefer direct providers over aggregators.
+	for _, candidate := range candidates {
+		if !candidate.isAggregator {
+			keys = appendUniqueKey(keys, candidate.key)
+		}
+	}
+	for _, candidate := range candidates {
+		if candidate.isAggregator {
+			keys = appendUniqueKey(keys, candidate.key)
+		}
+	}
	return appendRemainingTargetKeys(keys, targets), nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway.go` around lines 1714 - 1732, Update the fallback ordering around the
ranked-provider handling so direct unpriced providers are appended before
aggregators, matching the non-streaming positional fallback behavior. Apply this
ordering both when ranked is empty and when appending unranked candidates, while
preserving the existing skip error, deduplication, and remaining-target
handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker-compose.yml`:
- Around line 67-69: Update the Ollama provider mapping in the provider-list
definition to read the documented OLLAMA_CLOUD_API_KEY variable, while retaining
OLLAMA_API_KEY as a compatibility fallback if supported by the existing mapping
structure. Keep the surrounding Ollama provider configuration unchanged.

In `@gateway.go`:
- Around line 964-967: Snapshot g.catalog before constructing the cost-optimized
strategy, matching the existing providerSnap pattern, and pass the snapshot to
strategies.NewCostOptimized instead of g.catalog. Ensure the snapshot is
independent of the live catalog so Strategy.Execute and models.Calculate cannot
observe concurrent catalog mutations.

---

Outside diff comments:
In `@gateway.go`:
- Around line 1714-1732: Update the fallback ordering around the ranked-provider
handling so direct unpriced providers are appended before aggregators, matching
the non-streaming positional fallback behavior. Apply this ordering both when
ranked is empty and when appending unranked candidates, while preserving the
existing skip error, deduplication, and remaining-target handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 49996fc3-dfdb-44cc-9241-ecc569463f10

📥 Commits

Reviewing files that changed from the base of the PR and between 17998ae and bea3ca8.

📒 Files selected for processing (7)
  • docker-compose.yml
  • gateway.go
  • internal/strategies/costoptimized.go
  • internal/strategies/costoptimized_test.go
  • providers/factory.go
  • providers/ollama_cloud/ollama_cloud.go
  • providers/providers_list.go

Comment thread docker-compose.yml
Comment on lines +67 to +69
# - OLLAMA_CLOUD_API_KEY=...
# - OLLAMA_CLOUD_MODELS=gpt-oss:20b,gpt-oss:120b
# - OLLAMA_CLOUD_BASE_URL=https://ollama.com
# - OLLAMA_CLOUD_BASE_URL=https://api.ollama.com

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align the documented API-key variable with the provider mapping.

providers/providers_list.go:294-314 still reads the required key from OLLAMA_API_KEY, so configuring only the newly documented OLLAMA_CLOUD_API_KEY leaves the provider without credentials and causes initialization to fail. Update the provider mapping to use OLLAMA_CLOUD_API_KEY (or retain OLLAMA_API_KEY here for compatibility).

Suggested provider mapping
- {CfgKeyAPIKey, "OLLAMA_API_KEY", true},
+ {CfgKeyAPIKey, "OLLAMA_CLOUD_API_KEY", true},

This follows the provider wiring shown in providers/providers_list.go:294-314.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` around lines 67 - 69, Update the Ollama provider mapping
in the provider-list definition to read the documented OLLAMA_CLOUD_API_KEY
variable, while retaining OLLAMA_API_KEY as a compatibility fallback if
supported by the existing mapping structure. Keep the surrounding Ollama
provider configuration unchanged.

Comment thread gateway.go
Comment on lines +964 to +967
s = strategies.NewCostOptimized(targets, lookup, g.catalog, g.config.Strategy.UnpricedStrategy).
WithAggregatorPredicate(func(vk string) bool {
return providers.ProviderHasCapability(vk, providers.CapabilityAggregator)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent data race on g.catalog.

Passing g.catalog directly by reference creates a data race. The logic executed inside Strategy.Execute (which invokes the models.Calculate method against this map) runs without g.mu held, while the gateway concurrently mutates or replaces the catalog map in place (e.g., during config reloads, as implied by g.Catalog() relying on maps.Copy). You should snapshot it similarly to how providerSnap is handled above.

🐛 Proposed fix
-		s = strategies.NewCostOptimized(targets, lookup, g.catalog, g.config.Strategy.UnpricedStrategy).
+		s = strategies.NewCostOptimized(targets, lookup, maps.Clone(g.catalog), g.config.Strategy.UnpricedStrategy).
			WithAggregatorPredicate(func(vk string) bool {
				return providers.ProviderHasCapability(vk, providers.CapabilityAggregator)
			})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
s = strategies.NewCostOptimized(targets, lookup, g.catalog, g.config.Strategy.UnpricedStrategy).
WithAggregatorPredicate(func(vk string) bool {
return providers.ProviderHasCapability(vk, providers.CapabilityAggregator)
})
s = strategies.NewCostOptimized(targets, lookup, maps.Clone(g.catalog), g.config.Strategy.UnpricedStrategy).
WithAggregatorPredicate(func(vk string) bool {
return providers.ProviderHasCapability(vk, providers.CapabilityAggregator)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gateway.go` around lines 964 - 967, Snapshot g.catalog before constructing
the cost-optimized strategy, matching the existing providerSnap pattern, and
pass the snapshot to strategies.NewCostOptimized instead of g.catalog. Ensure
the snapshot is independent of the live catalog so Strategy.Execute and
models.Calculate cannot observe concurrent catalog mutations.

@gr3enarr0w

Copy link
Copy Markdown
Contributor Author

Addressed all four points:

  1. Test didn't exercise the bug it named: TestCostOptimized_AggregatorExcludedUnderAllowMode now uses a catalog where agg/gpt-4o carries a real, low price (not absent), so it actually exercises "a priced aggregator that looks cheapest under allow mode" instead of both candidates silently resolving unpriced against buildCatalog().
  2. Scope creep: split the net-new NanoGPT provider and the OLLAMA_API_KEYOLLAMA_CLOUD_API_KEY rename back out. NanoGPT already ships separately in feat(providers): add NanoGPT aggregator provider #240; the rename is reverted to match what's now on main (via v1.1.17 - Provider Readiness Closeout #328) to avoid the conflict you flagged. This PR is now just the aggregator cost-ranking exclusion + tagging OpenRouter with CapabilityAggregator.
  3. Rationale wording: reworded all four "platform markup" comments (package doc, WithAggregatorPredicate, the two inline comments in costoptimized.go) plus the test comments. The reason is now: an aggregator routes each request to whichever underlying provider/model it picks at call time, so a single static catalog price can't represent the actual per-request cost — not a hidden markup.
  4. CI: dug into this — there are zero GitHub Actions runs recorded for this branch at all, not even a pending one (compare feat(providers): add ZAI provider #239, which has full CI history). That's the "first run from this fork branch needs a maintainer to approve it" gate — nothing ever got queued. Snyk is a separate app-based check, which is why it's the only thing that showed green. I can't approve/trigger that myself; would appreciate a click on "Approve and run workflows" when you get a chance so Go tests/lint actually run.

Full local test suite (build, vet, race, short) passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(strategies/cost): add CapabilityAggregator to prevent opaque-cost misrouting in CostOptimized

2 participants