Skip to content

feat(models-info): add @vymalo/opencode-models-info plugin#18

Merged
stephane-segning merged 5 commits into
mainfrom
claude/adoring-edison-c24ce9
May 27, 2026
Merged

feat(models-info): add @vymalo/opencode-models-info plugin#18
stephane-segning merged 5 commits into
mainfrom
claude/adoring-edison-c24ce9

Conversation

@stephane-segning
Copy link
Copy Markdown
Contributor

Summary

  • New workspace package @vymalo/opencode-models-info — second plugin that enriches existing model entries with full metadata (context length, output limit, USD/M-token cost, modalities, tool_call / reasoning / attachment flags) by fetching from a provider-supplied OpenRouter-shaped endpoint declared via options.meta.modelsInfoUrl.
  • Auth-agnostic: runs as a Hooks.config hook after other plugins have populated providers and headers, so it composes with @vymalo/opencode-oauth2, static API keys, or any other auth scheme without depending on any of them. Upstream values are never overwritten.
  • Includes a TTL'd two-layer cache (in-memory + disk, atomic writes, ETag honored) mirroring the oauth2 cache pattern, with stale-on-failure fallback so a flaky endpoint never blocks OpenCode startup.
  • Includes a plan doc at plans/models-info-plan.md describing the design decisions, contract, and field mapping.

Test plan

  • pnpm --filter @vymalo/opencode-models-info typecheck — clean
  • pnpm --filter @vymalo/opencode-models-info test — 19 tests across mapping / cache / plugin
  • pnpm -r typecheck + pnpm -r test — workspace stays green (120 tests total)
  • pnpm -r build — all packages compile
  • Biome lint on the new package — 0 warnings
  • Manual end-to-end against a real OpenRouter-shaped endpoint (deferred; reviewers can stand one up or wire to OpenRouter directly)

🤖 Generated with Claude Code

Add a second OpenCode plugin that enriches existing model entries with
full metadata (context length, output limit, USD/M-token cost,
modalities, tool_call/reasoning/attachment flags) by fetching from a
provider-supplied OpenRouter-shaped endpoint declared via
`options.meta.modelsInfoUrl`.

Auth-agnostic by design: runs as a Hooks.config hook after other plugins
have populated providers and headers, so it composes with
@vymalo/opencode-oauth2, static API keys, or any other auth scheme
without depending on any of them. Upstream values are never overwritten.

Includes a TTL'd two-layer cache (in-memory + disk, atomic writes, ETag
honored) mirroring the oauth2 cache pattern, with stale-on-failure
fallback so a flaky endpoint never blocks OpenCode startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the @vymalo/opencode-models-info plugin, which enriches OpenCode model entries with metadata fetched from an OpenRouter-shaped endpoint. It features a file-based cache store, robust mapping logic, and a config hook to process providers in parallel. The review feedback highlights critical improvements to ensure the plugin is truly auth-agnostic by extracting and forwarding the provider's configured headers, merging them with specific metadata headers. Additionally, the reviewer suggests wrapping the provider enrichment process in a try-catch block to prevent errors from being silently swallowed during parallel execution, and points out a potential path duplication bug in the relative URL resolution logic.

Comment on lines +48 to +91
async function enrichProvider(
providerId: string,
providerConfig: ProviderConfigLike | undefined,
deps: EnrichDeps
): Promise<void> {
if (!providerConfig) {
return;
}
const opts = parseMetaOptions(providerConfig.options);
if (!opts) {
return;
}
const models = providerConfig.models;
if (!models || Object.keys(models).length === 0) {
deps.logger.debug("models_info_provider_skipped_no_models", { providerId });
return;
}

const record = await loadRecord(providerId, opts, deps);
if (!record) {
return;
}

const byId = new Map<string, OpenRouterModel>(record.models.map((m) => [m.id, m]));

let enrichedCount = 0;
for (const [modelId, modelConfig] of Object.entries(models)) {
const declaredId = typeof modelConfig.id === "string" ? modelConfig.id : undefined;
const match = byId.get(modelId) ?? (declaredId ? byId.get(declaredId) : undefined);
if (!match) {
continue;
}
const derived = mapOpenRouterEntry(match);
mergeIntoModel(modelConfig, derived);
enrichedCount += 1;
}

deps.logger.info("models_info_enriched", {
providerId,
enrichedCount,
totalModels: Object.keys(models).length,
sourceModels: record.models.length
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To satisfy the design goal of being auth-agnostic and reusing the provider's resolved authentication headers (e.g., from @vymalo/opencode-oauth2 or static API keys), we need to extract and pass the provider's configured headers to loadRecord so they can be forwarded in the fetch request. Additionally, since enrichConfig runs all providers in parallel using Promise.allSettled and does not inspect or log the rejected promises, any unexpected error thrown inside enrichProvider (such as a disk write failure in FileCacheStore.put or a network/parsing error) will be silently swallowed without any log output. Wrapping the body of enrichProvider in a try-catch block ensures that any failure is logged via deps.logger.error.

async function enrichProvider(
  providerId: string,
  providerConfig: ProviderConfigLike | undefined,
  deps: EnrichDeps
): Promise<void> {
  try {
    if (!providerConfig) {
      return;
    }
    const opts = parseMetaOptions(providerConfig.options);
    if (!opts) {
      return;
    }
    const models = providerConfig.models;
    if (!models || Object.keys(models).length === 0) {
      deps.logger.debug("models_info_provider_skipped_no_models", { providerId });
      return;
    }

    const providerHeaders = providerConfig.options?.headers as Record<string, string> | undefined;
    const record = await loadRecord(providerId, opts, providerHeaders, deps);
    if (!record) {
      return;
    }

    const byId = new Map<string, OpenRouterModel>(record.models.map((m) => [m.id, m]));

    let enrichedCount = 0;
    for (const [modelId, modelConfig] of Object.entries(models)) {
      const declaredId = typeof modelConfig.id === "string" ? modelConfig.id : undefined;
      const match = byId.get(modelId) ?? (declaredId ? byId.get(declaredId) : undefined);
      if (!match) {
        continue;
      }
      const derived = mapOpenRouterEntry(match);
      mergeIntoModel(modelConfig, derived);
      enrichedCount += 1;
    }

    deps.logger.info("models_info_enriched", {
      providerId,
      enrichedCount,
      totalModels: Object.keys(models).length,
      sourceModels: record.models.length
    });
  } catch (error) {
    deps.logger.error("models_info_enrichment_failed", {
      providerId,
      error: error instanceof Error ? error.message : String(error)
    });
  }
}

Comment on lines +93 to +111
async function loadRecord(
providerId: string,
opts: MetaProviderOptions,
deps: EnrichDeps
): Promise<CachedModelsRecord | undefined> {
const key = cacheKey(providerId, opts.modelsInfoUrl);
const now = deps.now ? deps.now() : Date.now();
const cached = await deps.cache.get(key);

if (cached && !isExpired(cached, now)) {
deps.logger.debug("models_info_cache_hit", {
providerId,
url: opts.modelsInfoUrl,
ageMs: now - cached.fetchedAt
});
return cached;
}

const headers = buildFetchHeaders(opts);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Update loadRecord signature to accept the provider's resolved headers and forward them to buildFetchHeaders.

async function loadRecord(
  providerId: string,
  opts: MetaProviderOptions,
  providerHeaders: Record<string, string> | undefined,
  deps: EnrichDeps
): Promise<CachedModelsRecord | undefined> {
  const key = cacheKey(providerId, opts.modelsInfoUrl);
  const now = deps.now ? deps.now() : Date.now();
  const cached = await deps.cache.get(key);

  if (cached && !isExpired(cached, now)) {
    deps.logger.debug("models_info_cache_hit", {
      providerId,
      url: opts.modelsInfoUrl,
      ageMs: now - cached.fetchedAt
    });
    return cached;
  }

  const headers = buildFetchHeaders(opts, providerHeaders);

Comment on lines +164 to +166
function buildFetchHeaders(opts: MetaProviderOptions): Record<string, string> | undefined {
return opts.modelsInfoHeaders;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Merge the provider's general headers with the specific modelsInfoHeaders (with modelsInfoHeaders taking precedence) to ensure authenticated requests work correctly.

Suggested change
function buildFetchHeaders(opts: MetaProviderOptions): Record<string, string> | undefined {
return opts.modelsInfoHeaders;
}
function buildFetchHeaders(
opts: MetaProviderOptions,
providerHeaders: Record<string, string> | undefined
): Record<string, string> | undefined {
if (!providerHeaders && !opts.modelsInfoHeaders) {
return undefined;
}
return {
...(providerHeaders ?? {}),
...(opts.modelsInfoHeaders ?? {})
};
}

return candidate;
}
const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
const rel = candidate.startsWith("/") ? candidate.slice(1) : candidate;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Stripping the leading slash from candidate to resolve it relative to the baseURL path works for /models/info, but it will cause duplication of path segments if the candidate already includes the base path (e.g., if baseURL is https://example.com/v1 and modelsInfoUrl is /v1/models/info, it resolves to https://example.com/v1/v1/models/info). This contradicts the plan's suggestion that authors can write "modelsInfoUrl": "/v1/models/info".

Consider checking if the candidate already starts with the base URL's path segment before stripping, or standardizing on standard URL resolution where leading slashes resolve from the host root.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1918e0c5a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +164 to +166
function buildFetchHeaders(opts: MetaProviderOptions): Record<string, string> | undefined {
return opts.modelsInfoHeaders;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include provider headers in models-info fetches

The request headers are built from meta.modelsInfoHeaders only, so existing provider headers (for example provider.options.headers.Authorization or API-key headers) are dropped before calling fetchOpenRouterModels. In authenticated deployments this causes /models/info calls to fail (typically 401/403), which prevents enrichment and contradicts the plugin’s auth-agnostic behavior for providers that already have working request headers.

Useful? React with 👍 / 👎.

Comment on lines +137 to +138
const refreshed: CachedModelsRecord = { ...cached, fetchedAt: now };
await deps.cache.put(key, refreshed);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh TTL from config when handling 304 responses

When a cached entry is revalidated with 304 Not Modified, the code copies the old record unchanged except for fetchedAt, so ttlSeconds remains whatever value was stored previously. If meta.modelsInfoTtlSeconds is changed in config, that new TTL is ignored until a future 200 response, causing cache freshness behavior to diverge from user configuration.

Useful? React with 👍 / 👎.

Comment on lines +67 to +73
return body.filter(isOpenRouterModel);
}
if (body && typeof body === "object") {
const data = (body as OpenRouterModelsResponse).data;
if (Array.isArray(data)) {
return data.filter(isOpenRouterModel);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject malformed model arrays instead of caching empty set

normalizeResponse silently filters out invalid entries and still returns success, so a malformed data array (for example, items missing string id) can become [] and be cached as a fresh snapshot. That overwrites previously valid cached metadata and disables enrichment until TTL expiry, instead of falling back to stale data on a parse error.

Useful? React with 👍 / 👎.

Unit tests:
- test/config.test.ts — parseMetaOptions edge cases (relative URL
  resolution, header coercion, defaulting, invalid inputs)
- test/fetcher.test.ts — fetchOpenRouterModels (envelope vs bare-array,
  ETag round-trip, header merging, timeout via AbortController,
  non-throwing error results)

Test env (reusable across the workspace):
- test-env/docker-compose.yml — WireMock service with healthcheck on
  port 18080 (Keycloak placeholder commented in, ready for the oauth2
  integration suite later).
- test-env/wiremock/ — OpenRouter-shaped catalog fixture + mappings
  covering happy-path 200, ETag-aware 304, and a Bearer-required
  ?auth=required variant for header-propagation tests.
- test-env/README.md — quick start, endpoints, stub-editing notes.

Integration suite:
- packages/opencode-models-info/test/integration/*.integration.test.ts
  — runs against the live WireMock; verifies enrichment, disk cache +
  304 path, header propagation, and 401-on-missing-auth fallback.
- Separate vitest.integration.config.ts so the default `pnpm test`
  stays hermetic and fast.
- Tests skip themselves when INTEGRATION_MODELS_INFO_URL is unset.

Scripts:
- Root: test:env:up / test:env:down (with --wait healthcheck gating) and
  test:integration which orchestrates up → all packages' integration
  suites (via `pnpm -r --if-present`) → down.
- Package: test:integration runs the integration vitest config.

Docs:
- CLAUDE.md — new repo-wide guide for future Claude Code sessions,
  including the integration env section.
- packages/opencode-models-info/README.md — Testing section with both
  unit and integration recipes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68daa86d74

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

etag: result.etag,
models: result.models
};
await deps.cache.put(key, next);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve fetched data when cache writes fail

In environments where the cache directory is read-only or cannot be created (for example a locked-down container with an unwritable $HOME/XDG_CACHE_HOME), FileCacheStore.put throws here after a successful fetch, causing enrichProvider to reject before returning next; Promise.allSettled then silently drops the provider and no models are enriched even though the network response was usable. The cache should be best-effort, so a failed write should be logged/ignored while still using the freshly fetched record.

Useful? React with 👍 / 👎.

opts: MetaProviderOptions,
deps: EnrichDeps
): Promise<CachedModelsRecord | undefined> {
const key = cacheKey(providerId, opts.modelsInfoUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include response-shaping headers in the cache key

When meta.modelsInfoHeaders changes for the same provider and URL (for example switching an x-tenant header or adding an auth header that exposes a different catalog), this cache key still hits the old snapshot and skips the fetch until the previous TTL expires, so model metadata from the prior header context can be applied to the new configuration for up to the default 24 hours. Either the cache key or cached record needs to account for headers that can vary the response.

Useful? React with 👍 / 👎.

Extend the existing publish workflow to also build, pack, verify, and
publish @vymalo/opencode-models-info alongside the oauth2 plugin.

- Tarball verification mirrors the oauth2 checks (dist/index.js present,
  src/test/node_modules absent, LICENSE present).
- Each package's publish step is now guarded by an `npm view` check —
  if the version in package.json is already on the registry, the step
  logs a notice and exits 0 instead of failing the whole workflow. Means
  a release that only bumps one package's version no longer fails on the
  unchanged one.
- Release-attachment glob updated to `dist-tarball/**/*.tgz` so both
  packages' tarballs are uploaded.

Also pick up biome format --write auto-fixes across the new package's
source and tests — purely cosmetic, makes `pnpm format:check` green so
the publish workflow's format-check gate stays clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66dfb841c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +59 to +60
const params = entry.supported_parameters ?? [];
const paramSet = new Set(params.map((p) => p.toLowerCase()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard supported_parameters before mapping

When a fetched catalog entry has a string or otherwise non-array supported_parameters field, normalizeResponse still accepts it because it only checks id, and this .map() then throws. In that scenario one malformed model from an external models-info endpoint makes enrichProvider reject and Promise.allSettled silently leaves the whole provider un-enriched, including any valid models in the same response; coerce with Array.isArray or validate this field before caching/mapping.

Useful? React with 👍 / 👎.

Comment on lines +72 to +73
if (inputMods.some((m) => m !== "text")) {
out.attachment = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve attachment capability for file inputs

When the provider advertises a non-text modality that OpenCode cannot put in modalities.input (for example OpenRouter's file), filterModalities drops it before this attachment check runs, so a model with input_modalities: ["text", "file"] is left without attachment: true. That makes file-capable models look text-only to OpenCode even though the response explicitly advertised attachment support; derive the attachment flag from the original input modalities before filtering.

Useful? React with 👍 / 👎.

Comment on lines +125 to +128
deps.logger.info("models_info_fetched", {
providerId,
url: opts.modelsInfoUrl,
count: result.models.length
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Redact secrets before logging modelsInfoUrl

When meta.modelsInfoUrl is an absolute URL that carries credentials in userinfo or query parameters (for example a signed URL or ?api_key=...), this logs the raw URL to both console fallback and client.app.log; the existing redaction only keys off field names and url is not redacted. In that configuration a routine successful fetch exposes the secret in OpenCode logs, so log a sanitized URL or omit query/userinfo.

Useful? React with 👍 / 👎.

stephane-segning and others added 2 commits May 27, 2026 11:56
Addresses inline review comments from gemini-code-assist and codex on PR #18.

H1 — buildFetchHeaders now merges provider.options.headers ⨁
meta.modelsInfoHeaders (meta wins on conflict). Restores the auth-agnostic
claim: static API keys and any plugin that has already populated
provider.options.headers (e.g. @vymalo/opencode-oauth2 ≥ 0.4.0) flow through
naturally without coupling.

H2 — enrichProvider wrapped in try/catch. Promise.allSettled was silently
swallowing any throw (disk write failures, mapping bugs); now surfaced via
deps.logger.error("models_info_enrichment_failed").

M1 — resolveUrl uses standard WHATWG URL semantics. Previously stripped
the leading slash from the candidate before resolving, which produced
`https://x.test/v1/v1/models` when a user wrote `modelsInfoUrl: "/v1/models"`
under `baseURL: "https://x.test/v1"`. Now:
  - `modelsInfoUrl: "models/info"` joins under baseURL's path (recommended).
  - `modelsInfoUrl: "/models/info"` is origin-rooted (escape hatch for
    metadata endpoints at a different path than the inference API).
README and JSDoc updated; the integration test fixture was already
absolute-URL based so untouched.

M2 — 304 path applies the current TTL from config instead of carrying
over the stored ttlSeconds. A tightened TTL takes effect on the next
revalidation, not on the next full 200.

M3 — fetcher treats "non-empty input filtered down to empty" as a parse
error. A malformed catalog response (entries missing string `id`) no
longer overwrites previously good cached data with []; the stale-cache
fallback kicks in instead.

M4 — cache.put is now best-effort. A read-only $HOME / cache dir
previously made the fetched record evaporate; now we log
`models_info_cache_write_failed` and still return the in-memory record
so enrichment proceeds.

M5 — cache key includes the meta.modelsInfoHeaders fingerprint. Switching
an `x-tenant` header (or any user-specified meta header) busts the
cache. Provider-level headers are intentionally NOT keyed in — a
rotating OAuth2 bearer would otherwise thrash the cache on every
refresh.

Test coverage grew 39 → 52 unit tests; integration still 4. New cases
cover header propagation + override, error-log on cache failure, parse
error vs empty catalog, best-effort cache write, TTL refresh on 304, and
header-keyed cache partitioning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… time

At the end of the config hook, stamp the cached access token onto
config.provider[id].options.headers.Authorization for every managed
provider. This makes the token visible to subsequent config-time
consumers — most notably @vymalo/opencode-models-info, which can now
fetch from an OAuth2-protected `meta.modelsInfoUrl` by inheriting
options.headers (no coupling between the two plugins).

Safety:
- Never overwrites a user-set Authorization header (case-insensitive
  check covers both `Authorization` and `authorization`).
- Only propagates when the cached token is still valid (30s skew before
  declared expiry). Avoids handing a known-stale token to a sibling
  plugin's config-time fetch.
- The chat.headers hook continues to overwrite per-request with a
  freshly-ensured token, so any drift between config-time and chat-time
  only ever affects other config-time consumers — never the actual
  inference call.

Logs `oauth2_bearer_propagated_to_provider_headers` (debug) on
propagation and `oauth2_bearer_propagation_skipped_user_set` when a
user-supplied header wins.

Adds two regression tests:
- propagates the cached bearer into provider.options.headers
- does not overwrite a user-set Authorization header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0532895dd1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +426 to +429
const hasUserAuth = Object.keys(headers).some((k) => k.toLowerCase() === "authorization");
if (hasUserAuth) {
logger.debug("oauth2_bearer_propagation_skipped_user_set", { providerId });
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh OAuth-propagated Authorization headers

When the same config object is processed again after the runtime token has been refreshed (for example by chat.headers or the sync scheduler), this guard treats the Authorization header that this hook inserted on the previous run as user-supplied and returns before writing the new cached token. Downstream config-time consumers such as @vymalo/opencode-models-info then keep inheriting the stale bearer and can fail with 401/403 until the config object is rebuilt; track whether the header was plugin-owned or overwrite the previously propagated value when the cached token changes.

Useful? React with 👍 / 👎.

Comment on lines +39 to +41
"files": [
"dist"
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include LICENSE in the models-info package

In the new models-info package, files only whitelists dist, and this package directory does not contain its own LICENSE; npm's packlist includes package.json/README and whitelisted files, but not the repository-root LICENSE for this workspace package (confirmed with npm pack --dry-run, which listed README.md/package.json/dist only). The publish workflow now verifies dist-tarball/models-info/*.tgz and exits when package/LICENSE is absent, so the first release including this package will fail in “Verify tarball contents” before publishing.

Useful? React with 👍 / 👎.

Comment on lines +161 to +163
if npm view "@vymalo/opencode-oauth2@${version}" version >/dev/null 2>&1; then
echo "::notice::@vymalo/opencode-oauth2@${version} already on npm — skipping."
exit 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish the oauth2 changes instead of skipping them

This commit changes packages/opencode-oauth2/src/opencode.ts, but packages/opencode-oauth2/package.json remains at 0.3.0; with this newly added registry check, a release where @vymalo/opencode-oauth2@0.3.0 already exists exits before pnpm publish. In that common release path the OAuth bearer propagation needed by the new models-info auth composition is never shipped, even though the models-info README tells users to rely on it, so either bump the oauth2 package version or avoid changing it in a release that will skip that package.

Useful? React with 👍 / 👎.

@stephane-segning stephane-segning merged commit 21080d8 into main May 27, 2026
1 of 3 checks passed
@stephane-segning stephane-segning deleted the claude/adoring-edison-c24ce9 branch May 28, 2026 00:21
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.

1 participant