Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b453a7
feat(discovery): register OpenCode Go as its own ProviderModelSource
seonghobae Sep 2, 2026
aeb6b35
test(discovery): keep paid Go subscription out of free pool
seonghobae Sep 2, 2026
65092c8
fix(discovery): key credential rollback on (credential, source) pairs
Sep 3, 2026
26d804f
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Sep 3, 2026
29f65ef
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae Sep 4, 2026
5cfcb7a
Merge remote-tracking branch 'origin/main' into codex/repair-pr1031-o…
seonghobae Sep 4, 2026
3290f19
docs(discovery): record shared OpenCode credential contract
seonghobae Sep 4, 2026
0129404
Merge commit '654482ddc037752ba4c6c38e8220f00c7e4ab40b' into codex/re…
seonghobae Sep 4, 2026
9d23a79
fix(discovery): admit only compatible OpenCode Go models
seonghobae Sep 4, 2026
0a3badd
Merge commit 'bec466d2' into codex/repair-pr1031-opencode-go-hardening
seonghobae Sep 4, 2026
481911b
test(discovery): restore the Go parser fixtures the npm gate invalidated
seonghobae Sep 5, 2026
cd191b9
fix(discovery): fall back to the provider adapter when a model names …
seonghobae Sep 5, 2026
89ece3e
Merge branch 'main' into feat/opencode-go-provider-source
opencode-agent[bot] Sep 5, 2026
1b1ecf6
Merge main and reconcile OpenCode Go shared credential contracts
seonghobae Sep 24, 2026
0e0e45a
test: use current psychometric candidate IDs
seonghobae Sep 26, 2026
be2a2b5
test: accept deployment-scoped psychometric IDs
seonghobae Sep 26, 2026
5d0d362
docs: reserve ADR 0136 for OpenCode Go
seonghobae Sep 26, 2026
1f07f2d
fix: address OpenCode Go review findings
seonghobae Sep 26, 2026
aa17a2b
test: preserve bootstrap failure boundaries after eligibility repair
seonghobae Sep 26, 2026
542f0e6
Merge protected main CI repairs into OpenCode Go discovery
seonghobae Sep 27, 2026
8825a24
Merge remote-tracking branch 'origin/main' into feat/opencode-go-prov…
seonghobae Sep 27, 2026
543c1b1
Merge remote-tracking branch 'origin/main' into feat/opencode-go-prov…
seonghobae Sep 27, 2026
dd625f9
Merge remote-tracking branch 'origin/main' into feat/opencode-go-prov…
seonghobae Sep 27, 2026
481c889
docs(discovery): reserve ADR 0137 after main integration
seonghobae Sep 27, 2026
e3b81bb
Merge remote-tracking branch 'origin/main' into feat/opencode-go-prov…
seonghobae Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.d/opencode-go-shared-credential.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OpenCode Go discovery now reuses the OpenCode Zen KV credential while keeping its catalog and paid-subscription routing evidence separate.
56 changes: 47 additions & 9 deletions contextual_orchestrator/model_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@ class ProviderModelSource:
bootstrap_required: bool = True
evidence_only: bool = False
models_dev_provider_id: str | None = None
required_models_dev_npm: str | None = None
# True when serving this source at all costs money on a recurring plan
# (OpenCode Go), independently of the per-token rates its catalog
# reports. Such an endpoint reports zero token rates because tokens are
# included in the subscription -- not because serving is free -- so
# ``_parse_openai_compatible`` publishes those rates as unknown and never
# marks the row free. See the ``opencode_go`` source below.
requires_paid_subscription: bool = False


def configured_gateway_source(
Expand Down Expand Up @@ -426,12 +434,26 @@ def configured_gateway_source(
models_dev_provider_id="opencode",
),
ProviderModelSource(
# A separate paid subscription on top of Zen, at a distinct endpoint
# (/zen/go/v1/... vs Zen's own /zen/v1/...) with its own models.dev
# catalog ("opencode-go", 34 models, verified live -- distinct from
# "opencode"/Zen's 97). The credential is *not* separate: subscribing
# to Go unlocks these endpoints for the same OpenCode Zen account key
# (owner-confirmed 2026-09-03; see docs/product-technical-gap-baseline.md's
# "OpenCode Go endpoint is not registered" entry for the corrected
# investigation trail). `bootstrap_required=False` matches Zen's own
# optional-integration treatment -- not every OpenCode Zen account has
# a Go subscription, so a missing/failing Go catalog must not block
# startup any more than a missing Zen one does.
provider_name="opencode_go",
credential_name="OPENCODE_ZEN_API_KEY",
Comment thread
seonghobae marked this conversation as resolved.
Resolved
list_url="https://opencode.ai/zen/go/v1/models",
chat_base_url="https://opencode.ai/zen/go/v1",
capabilities=("chat",),
bootstrap_required=False,
models_dev_provider_id="opencode-go",
Comment thread
seonghobae marked this conversation as resolved.
Resolved
required_models_dev_npm="@ai-sdk/openai-compatible",
requires_paid_subscription=True,
),
ProviderModelSource(
provider_name="nvidia_nim",
Expand Down Expand Up @@ -604,7 +626,7 @@ def _fetch_models_dev_metadata(*, timeout: float) -> Any | None:
"""Fetch the shared Models.dev catalog with a small bounded retry.

Every ``models_dev_provider_id``-joined source (``opencode_zen``,
``nvidia_nim``, ``nvidia_nim_sub``, ``openai``) shares this one
``opencode_go``, ``nvidia_nim``, ``nvidia_nim_sub``, ``openai``) shares this one
unauthenticated, best-effort, third-party fetch for its free-cost
evidence; none of those providers report their own pricing, so a lone
transient failure here (a timeout, a reset connection, or the
Expand Down Expand Up @@ -1309,6 +1331,11 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[
model_id = row.get("id")
if type(model_id) is not str or not model_id:
continue
if (
source.required_models_dev_npm
and row.get("_models_dev_npm") != source.required_models_dev_npm
):
continue
pricing = row.get("pricing") if isinstance(row.get("pricing"), dict) else {}
architecture = row.get("architecture") if isinstance(row.get("architecture"), dict) else {}
supported_parameters = (
Expand Down Expand Up @@ -1388,6 +1415,22 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[
if key in UNIT_PRICE_DIMENSIONS
and _valid_price_component(value)
)
is_free = _row_is_free(row, pricing=pricing, inputs=inputs, outputs=outputs)
if source.requires_paid_subscription:
# The recurring plan is what serving costs here; a zero token rate
# only means tokens are included in it, never that serving is
# free. Publishing that zero as a *known* price would admit a
# subscription-gated model into every zero-cost route -- the
# ``cost:free`` serving tag and, independently of it,
# ``refresh_price_book`` feeding ``_is_free_agent``'s price-only
# branch (which reads the price book, not ``is_free``, so
# clearing the tag alone would not keep it out). Report a zero
# component as unknown instead -- the same honesty rule Bytez's
# GPU-second pricing follows below. A genuinely non-zero rate is
# real evidence and is kept.
is_free = False
prompt_price = prompt_price or None
completion_price = completion_price or None
discovered.append(
DiscoveredModel(
provider_name=source.provider_name,
Expand All @@ -1411,12 +1454,7 @@ def _parse_openai_compatible(payload: Any, source: ProviderModelSource) -> list[
prompt_price_per_1k=prompt_price,
completion_price_per_1k=completion_price,
unit_prices=unit_prices,
is_free=_row_is_free(
row,
pricing=pricing,
inputs=inputs,
outputs=outputs,
),
is_free=is_free,
supports_zero_data_retention=(
row["supports_zero_data_retention"]
if isinstance(row.get("supports_zero_data_retention"), bool)
Expand Down Expand Up @@ -1726,8 +1764,8 @@ def discover_all_models(
One provider's failure never blocks the others: errors are collected and
returned alongside whatever models were successfully discovered.

Up to four sources (``opencode_zen``, ``nvidia_nim``, ``nvidia_nim_sub``,
``openai``) each want the same Models.dev catalog. When any registered
Up to five sources (``opencode_zen``, ``opencode_go``, ``nvidia_nim``,
``nvidia_nim_sub``, ``openai``) each want the same Models.dev catalog. When any registered
source declares ``models_dev_provider_id``, fetch it here exactly once
(:func:`_fetch_models_dev_metadata`, with its own small bounded retry) and
hand every source the identical parsed payload, instead of each source
Expand Down
78 changes: 56 additions & 22 deletions contextual_orchestrator/provider_catalog_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,30 @@ class of regression this function exists to prevent. A name reaching
if not to_evaluate:
return ProviderCredentialInventoryVerdict(True, None, None)

provider_by_credential = {
source.credential_name: source.provider_name for source in provider_model_sources
}
# Several sources can share one credential (OpenCode Zen and the optional
# Go subscription both use OPENCODE_ZEN_API_KEY), so this is a one-to-many
# map: a one-to-one one silently kept only the last-declared source and
# judged every rollback of that credential against the wrong provider's
# evidence -- reporting a real, classified Zen failure as an "unexplained
# rollback" hard-fail. A credential with no declared source keeps the
# previous fallback of standing in as its own provider name.
providers_by_credential: dict[str, set[str]] = {}
for source in provider_model_sources:
providers_by_credential.setdefault(source.credential_name, set()).add(
source.provider_name
)
providers_with_errors = {
name for name in report.get("providers_with_errors", ()) if isinstance(name, str)
}
error_classifications = dict(report.get("provider_error_classifications", {}) or {})

def failing_providers(credential_name: str) -> list[str]:
"""Return the sorted sources sharing ``credential_name`` that failed."""
return sorted(
providers_by_credential.get(credential_name, {credential_name})
& providers_with_errors
)

unconfigured = sorted(
name for name in to_evaluate if not (environ.get(name) or "").strip()
)
Expand All @@ -294,28 +310,31 @@ class of regression this function exists to prevent. A name reaching
None,
)

unexplained = sorted(
name
for name in to_evaluate
if provider_by_credential.get(name) not in providers_with_errors
)
unexplained = sorted(name for name in to_evaluate if not failing_providers(name))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if unexplained:
return ProviderCredentialInventoryVerdict(
False,
f"credential inventory mismatch: unexplained rollback for: {unexplained}",
None,
)

# Every failed source sharing the credential must be individually
# tolerable: one transient sibling never excuses another's auth failure.
non_transient = sorted(
name
for name in to_evaluate
if error_classifications.get(provider_by_credential.get(name, ""))
!= TRANSIENT_FAILURE_CLASSIFICATION
if any(
error_classifications.get(provider) != TRANSIENT_FAILURE_CLASSIFICATION
for provider in failing_providers(name)
)
)
if non_transient:
observed = {
name: error_classifications.get(
provider_by_credential.get(name, ""), UNKNOWN_FAILURE_CLASSIFICATION
name: next(
error_classifications.get(provider, UNKNOWN_FAILURE_CLASSIFICATION)
for provider in failing_providers(name)
if error_classifications.get(provider)
!= TRANSIENT_FAILURE_CLASSIFICATION
)
for name in non_transient
}
Expand All @@ -327,9 +346,11 @@ class of regression this function exists to prevent. A name reaching
)

# Credential-backed provider accounts remain independent here. Sharing a
# vendor or endpoint is not evidence that catalogs or failures are equal.
# vendor, endpoint, or credential is not evidence that catalogs or
# failures are equal, so this counts the distinct sources that actually
# failed -- two sources sharing one key are two degraded accounts.
affected_providers = sorted(
{provider_by_credential.get(name, name) for name in to_evaluate}
{provider for name in to_evaluate for provider in failing_providers(name)}
)
if len(affected_providers) > max_tolerated_missing_providers:
return ProviderCredentialInventoryVerdict(
Expand Down Expand Up @@ -577,16 +598,29 @@ def bootstrap_provider_catalog_runtime(
else 0
)
failed_provider_names = {error.provider_name for error in errors}
live_accounts = {_model_key(model) for model in live_models}
Comment thread
seonghobae marked this conversation as resolved.
Resolved
# One credential can back more than one provider source: OpenCode Zen
# and the optional OpenCode Go subscription both authenticate with
# OPENCODE_ZEN_API_KEY. A source failing is evidence about *that
# source*, never proof the shared credential itself is bad -- an
# account with a working Zen subscription and no Go entitlement gets
# an expected 401 (or an empty catalog) from Go every single run.
# Roll a credential back only when every source it backs failed, so
# that expected optional-source failure cannot delete the key the
# live Zen models authenticate with (which would drop them from
# ``usable_models`` below and hard-fail bootstrap outright for a
# Zen-only account).
sources_by_credential: dict[str, list[ProviderModelSource]] = {}
for source in source_tuple:
if source.credential_name in registered:
sources_by_credential.setdefault(source.credential_name, []).append(source)
failed_credentials = {
source.credential_name
for source in source_tuple
if source.credential_name in registered
and (
name
for name, credential_sources in sources_by_credential.items()
if all(
source.provider_name in failed_provider_names
or not any(
_model_key(model) == _source_key(source)
for model in live_models
)
or _source_key(source) not in live_accounts
for source in credential_sources
)
}
restored_credentials = _restore_provider_credentials_atomically(
Expand Down
14 changes: 11 additions & 3 deletions docs/kv-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,19 +306,27 @@ confidential-client secret placement remain deployment-controller operations.
Once a provider's credential is registered in the KV, `contextual_orchestrator`
can discover that provider's available models and turn them into agent-pool
candidates automatically — no hand-written `agents.json` entry required.
`contextual_orchestrator/model_discovery.py` covers six providers out of the
`contextual_orchestrator/model_discovery.py` covers seven provider sources out of the
box, all resolved through `get_credential` (never fabricated, never read from
`os.getenv`):

| Provider | KV credential name | Auth header |
| ------------------ | ------------------------ | ------------------ |
| OpenAI | `OPENAI_API_KEY` | `Bearer <token>` |
| OpenRouter | `OPENROUTER_API_KEY` | `Bearer <token>` |
| OpenCode Zen | `OPENCODE_ZEN_API_KEY` | `Bearer <token>` |
| OpenCode Go | `OPENCODE_ZEN_API_KEY` | `Bearer <token>` |
| NVIDIA NIM (primary)| `NVIDIA_NIM_API_KEY` | `Bearer <token>` |
| NVIDIA NIM (sub) | `NVIDIA_NIM_API_KEY_SUB` | `Bearer <token>` |
| Bytez | `BYTEZ_API_KEY` | `Key <token>` |
| Bytez | `BYTEZ_API_KEY` | `<token>` |
| Configured OpenAI-compatible gateway | `LLM_GATEWAY_API_KEY` | `Bearer <token>` |

OpenCode Go uses a distinct catalog and serving endpoint, but the same account
credential as OpenCode Zen. Register `OPENCODE_ZEN_API_KEY` once in the KV;
discovery never reads a second Go-specific secret from the environment. Only
models.dev rows assigned to the OpenAI-compatible protocol enter this chat
endpoint; Responses- and Anthropic-protocol rows fail closed.

For a configured gateway, the one-shot discovery/bootstrap boundary accepts
`LLM_GATEWAY_API_URL` (or the equivalent `LLM_GATEWAY_URL`) only when its HTTPS
host is listed in `CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS`. It promotes
Expand All @@ -330,7 +338,7 @@ When serving the persisted agents, pass the same host with
`--allowed-provider-host`; startup discovery reads this injected runtime policy
and never re-reads or promotes gateway environment values.

Bytez's `Key <token>` scheme (rather than `Bearer`) is why `ModelAgent` has an
Bytez's raw token scheme (rather than `Bearer`) is why `ModelAgent` has an
`auth_scheme` field (default `"Bearer"`) — set it per agent when a provider
doesn't use the OpenAI-compatible default.

Expand Down
Loading
Loading