@jacobbd/relay-ai 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -189,7 +189,7 @@ Opens a browser-based dashboard on a random local port. From the UI you can:
189
189
  - **Keep Claude Code's Anthropic login** — on the Claude Code CLI card, check **Keep my Anthropic login and add Relay models** to keep your normal Claude models while adding the selected Relay model and compatible favorites. This option is not shown for Claude Desktop.
190
190
  - **Manage General Favorites** — the sidebar shows your saved favorite models with a slot indicator (Slots used X/20). Favorites launch through all supported agents.
191
191
  - **Manage Antigravity Favorites** — separate favorites panel for Antigravity sessions.
192
- - **Manage providers** — add providers from templates, delete providers, and refresh model lists inline, all without leaving the browser.
192
+ - **Manage providers** — add providers from templates, delete providers, and refresh model lists inline, all without leaving the browser. For GitHub Copilot, ChatGPT, and xAI OAuth, the UI displays a one-time device code with **Copy code** and **Open sign-in page** buttons so you can complete sign-in without using the terminal. Connected Copilot cards also identify the account as Free, Paid, or Plan unverified so the visible model catalog is easier to understand.
193
193
  - **Run the Server tab** — configure and start the same gateway as `relay-ai server` (favorites-only or specific providers, discovery id masking, local/network listen mode) and see the resulting URLs, API key, and model catalog right in the browser. Runs in the same process as the UI, so it stops when you close the dashboard. See [Registry gateway (`relay-ai server`)](#registry-gateway-relay-ai-server) below for what each option does.
194
194
 
195
195
  Press `Ctrl+C` in the terminal where `relay-ai ui` is running to shut down the dashboard server (this also stops the gateway if you started it from the Server tab).
@@ -409,6 +409,8 @@ Read the full setup and risk notes in **[docs/ANTIGRAVITY.md](docs/ANTIGRAVITY.m
409
409
 
410
410
  relay-ai supports OAuth providers that use device-code sign-in, so you can connect an existing subscription without pasting an API key. See **[docs/SUBSCRIPTION-OAUTH.md](docs/SUBSCRIPTION-OAUTH.md)** for setup details.
411
411
 
412
+ In `relay-ai ui`, select one of the OAuth providers and click **Get sign-in code**. Relay AI shows the code with a Copy button; click **Open sign-in page**, paste the code in the provider's page, and return to the UI while it finishes connecting.
413
+
412
414
  Device code flows for existing subscriptions:
413
415
 
414
416
  ```bash
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getTemplateById,
4
4
  init_provider_templates
5
- } from "./chunk-DO5FAMNC.js";
5
+ } from "./chunk-MVBA7ABV.js";
6
6
 
7
7
  // src/constants.ts
8
8
  import { homedir } from "os";
@@ -11,7 +11,7 @@ import { join } from "path";
11
11
  // package.json
12
12
  var package_default = {
13
13
  name: "@jacobbd/relay-ai",
14
- version: "0.4.8",
14
+ version: "0.5.0",
15
15
  publishConfig: {
16
16
  access: "public"
17
17
  },
@@ -2409,7 +2409,8 @@ function decodeAuthEntry(value) {
2409
2409
  refresh: record["refresh"],
2410
2410
  expires: record["expires"],
2411
2411
  accountId: typeof record["accountId"] === "string" ? record["accountId"] : void 0,
2412
- enterpriseUrl: typeof record["enterpriseUrl"] === "string" ? record["enterpriseUrl"] : void 0
2412
+ enterpriseUrl: typeof record["enterpriseUrl"] === "string" ? record["enterpriseUrl"] : void 0,
2413
+ providerData: record["providerData"] && typeof record["providerData"] === "object" && !Array.isArray(record["providerData"]) ? record["providerData"] : void 0
2413
2414
  };
2414
2415
  }
2415
2416
  if (record["type"] === "wellknown" && typeof record["key"] === "string" && typeof record["token"] === "string") {
@@ -2460,13 +2461,14 @@ function oauthCredentialToKeychainJson(cred) {
2460
2461
 
2461
2462
  // src/oauth/types.ts
2462
2463
  function tokensToStoredCredential(tokens, existingRefresh, accountId, providerData) {
2464
+ const mergedProviderData = providerData || tokens.providerData ? { ...providerData, ...tokens.providerData } : void 0;
2463
2465
  return {
2464
2466
  type: "oauth",
2465
2467
  access: tokens.access_token,
2466
2468
  refresh: tokens.refresh_token ?? existingRefresh ?? "",
2467
2469
  expires: Date.now() + (tokens.expires_in ?? 3600) * 1e3,
2468
2470
  ...accountId ? { accountId } : {},
2469
- ...providerData ? { providerData } : {}
2471
+ ...mergedProviderData ? { providerData: mergedProviderData } : {}
2470
2472
  };
2471
2473
  }
2472
2474
  function parseStoredOAuthCredential(raw) {
@@ -2512,10 +2514,16 @@ var CLIENT_ID2 = "Iv1.b507a08c87ecfe98";
2512
2514
  var DEVICE_CODE_URL = "https://github.com/login/device/code";
2513
2515
  var TOKEN_URL2 = "https://github.com/login/oauth/access_token";
2514
2516
  var COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token";
2517
+ var COPILOT_USER_URL = "https://api.github.com/copilot_internal/user";
2515
2518
  var SCOPE = "copilot";
2516
2519
  var DEVICE_CODE_DEFAULT_INTERVAL_MS = 5e3;
2517
2520
  var DEVICE_CODE_DEFAULT_EXPIRES_MS2 = 15 * 60 * 1e3;
2518
2521
  var OAUTH_POLLING_SAFETY_MARGIN_MS2 = 1e3;
2522
+ var FREE_COPILOT_SKUS = /* @__PURE__ */ new Set([
2523
+ "free_limited_copilot",
2524
+ "free_educational_quota",
2525
+ "no_auth_limited_copilot"
2526
+ ]);
2519
2527
  function commonHeaders() {
2520
2528
  return {
2521
2529
  Accept: "application/json",
@@ -2523,6 +2531,46 @@ function commonHeaders() {
2523
2531
  "User-Agent": `relay-ai/${VERSION}`
2524
2532
  };
2525
2533
  }
2534
+ function classifyCopilotAccount(user) {
2535
+ const login = typeof user["login"] === "string" && user["login"].trim() ? user["login"].trim() : void 0;
2536
+ const sku = typeof user["access_type_sku"] === "string" && user["access_type_sku"].trim() ? user["access_type_sku"].trim() : void 0;
2537
+ const plan = typeof user["copilot_plan"] === "string" && user["copilot_plan"].trim() ? user["copilot_plan"].trim() : void 0;
2538
+ if (!sku && !plan) {
2539
+ return {
2540
+ ...login ? { login } : {},
2541
+ lookup_status: "unknown"
2542
+ };
2543
+ }
2544
+ const isFree = FREE_COPILOT_SKUS.has(sku?.toLowerCase() ?? "") || plan?.toLowerCase() === "free";
2545
+ return {
2546
+ ...login ? { login } : {},
2547
+ ...sku ? { access_type_sku: sku } : {},
2548
+ ...plan ? { copilot_plan: plan } : {},
2549
+ is_free_plan: isFree,
2550
+ lookup_status: "known"
2551
+ };
2552
+ }
2553
+ async function fetchCopilotAccount(ghuToken) {
2554
+ const response = await fetch(COPILOT_USER_URL, {
2555
+ method: "GET",
2556
+ headers: {
2557
+ Authorization: `Bearer ${ghuToken}`,
2558
+ Accept: "application/json",
2559
+ "User-Agent": `relay-ai/${VERSION}`,
2560
+ "Editor-Version": "vscode/1.85.1",
2561
+ "X-GitHub-Api-Version": "2025-04-01"
2562
+ }
2563
+ });
2564
+ if (!response.ok) {
2565
+ const detail = await response.text().catch(() => "");
2566
+ throw new Error(`GitHub Copilot account lookup failed (${response.status})${detail ? `: ${detail}` : ""}`);
2567
+ }
2568
+ const json = await response.json();
2569
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
2570
+ throw new Error("GitHub Copilot account lookup returned invalid JSON");
2571
+ }
2572
+ return classifyCopilotAccount(json);
2573
+ }
2526
2574
  async function requestGithubDeviceCode() {
2527
2575
  const response = await fetch(DEVICE_CODE_URL, {
2528
2576
  method: "POST",
@@ -2561,7 +2609,16 @@ async function exchangeForCopilotToken(ghuToken) {
2561
2609
  const expiresMs = new Date(json.expires_at).getTime() - Date.now();
2562
2610
  if (expiresMs > 0) expiresIn = Math.floor(expiresMs / 1e3);
2563
2611
  }
2564
- return { access_token: json.token, expires_in: expiresIn };
2612
+ let account = { lookup_status: "unknown" };
2613
+ try {
2614
+ account = await fetchCopilotAccount(ghuToken);
2615
+ } catch {
2616
+ }
2617
+ return {
2618
+ access_token: json.token,
2619
+ expires_in: expiresIn,
2620
+ providerData: { copilot: account }
2621
+ };
2565
2622
  }
2566
2623
  async function refreshGithubCopilotToken(ghuToken) {
2567
2624
  const copilot = await exchangeForCopilotToken(ghuToken);
@@ -2598,7 +2655,8 @@ async function pollGithubDeviceCodeToken(device, opts) {
2598
2655
  access_token: copilot.access_token,
2599
2656
  refresh_token: ghuToken,
2600
2657
  // store ghu_ as refresh for re-exchange later
2601
- expires_in: copilot.expires_in
2658
+ expires_in: copilot.expires_in,
2659
+ providerData: copilot.providerData
2602
2660
  };
2603
2661
  }
2604
2662
  if (error === "authorization_pending") {
@@ -3105,6 +3163,28 @@ async function resolveProviderOAuthProviderData(authRef, diag) {
3105
3163
  const raw = await readKeyringAccount(parsed.account, diag);
3106
3164
  return parseStoredOAuthCredential(raw)?.providerData;
3107
3165
  }
3166
+ async function enrichGithubCopilotOAuthProviderData(authRef, diag) {
3167
+ const parsed = parseAuthRef(authRef);
3168
+ if (!parsed || parsed.kind !== "keyring" || oauthProviderIdFromAccount(parsed.account) !== "github-copilot") {
3169
+ return void 0;
3170
+ }
3171
+ const raw = await readKeyringAccount(parsed.account, diag);
3172
+ const credential = parseStoredOAuthCredential(raw);
3173
+ if (!credential?.refresh) return credential?.providerData;
3174
+ try {
3175
+ const summary = await fetchCopilotAccount(credential.refresh);
3176
+ const providerData = { ...credential.providerData, copilot: summary };
3177
+ await writeKeyringAccount(
3178
+ parsed.account,
3179
+ oauthCredentialToKeychainJson({ ...credential, providerData }),
3180
+ diag
3181
+ );
3182
+ return providerData;
3183
+ } catch (err) {
3184
+ diag?.(`GitHub Copilot plan lookup unavailable \u2014 ${err instanceof Error ? err.message : String(err)}`);
3185
+ return credential.providerData;
3186
+ }
3187
+ }
3108
3188
  function decodeProviderSecret(raw) {
3109
3189
  if (!raw) return null;
3110
3190
  const trimmed = raw.trim();
@@ -3333,6 +3413,14 @@ function migrateOAuthXaiProvider(registry) {
3333
3413
  };
3334
3414
  return true;
3335
3415
  }
3416
+ function migrateAlibabaDashScopeChinaLabel(registry) {
3417
+ const provider = registry.providers.find(
3418
+ (p8) => p8.id === "alibaba" && p8.templateId === "alibaba" && p8.name === "Alibaba DashScope" && p8.api.url === "https://dashscope.aliyuncs.com/compatible-mode/v1"
3419
+ );
3420
+ if (!provider) return false;
3421
+ provider.name = "Alibaba DashScope (China)";
3422
+ return true;
3423
+ }
3336
3424
 
3337
3425
  // src/registry/validate.ts
3338
3426
  var PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -3443,6 +3531,7 @@ function loadRegistry(path = getProvidersPath()) {
3443
3531
  let migrated = migrateLegacyCloudProviders(registry);
3444
3532
  if (migrateOAuthOpenAiProvider(registry)) migrated = true;
3445
3533
  if (migrateOAuthXaiProvider(registry)) migrated = true;
3534
+ if (migrateAlibabaDashScopeChinaLabel(registry)) migrated = true;
3446
3535
  if (migrated) {
3447
3536
  try {
3448
3537
  saveRegistry(registry, path);
@@ -3520,11 +3609,15 @@ var TEMPLATE_TO_PRICING_PLATFORM = {
3520
3609
  openai: "openai",
3521
3610
  google: "google_ai_studio",
3522
3611
  alibaba: "alibaba",
3612
+ "qwen-cloud-payg": "alibaba",
3523
3613
  openrouter: "openrouter",
3524
3614
  anthropic: "anthropic",
3525
3615
  nvidia: "nvidia",
3526
3616
  venice: "openrouter"
3527
3617
  };
3618
+ var PRICING_OPT_OUT_TEMPLATE_IDS = /* @__PURE__ */ new Set([
3619
+ "qwen-cloud-token-plan"
3620
+ ]);
3528
3621
  function loadBundledPricingCache() {
3529
3622
  return pricing_cache_default;
3530
3623
  }
@@ -3647,13 +3740,27 @@ function enrichModelsWithPricing(models, index, platform) {
3647
3740
  return { ...model, cost, isFree: isFreeStatus(freeStatus), freeStatus };
3648
3741
  });
3649
3742
  }
3743
+ function enrichModelsForProviderPricing(models, index, templateId, providerId) {
3744
+ if (PRICING_OPT_OUT_TEMPLATE_IDS.has(templateId) || PRICING_OPT_OUT_TEMPLATE_IDS.has(providerId)) {
3745
+ return models.map(({ cost: _cost, isFree: _isFree, freeStatus: _freeStatus, ...model }) => model);
3746
+ }
3747
+ return enrichModelsWithPricing(
3748
+ models,
3749
+ index,
3750
+ pricingPlatformForProvider(templateId, providerId)
3751
+ );
3752
+ }
3650
3753
  function applyPricingToRegistryProviders(registry, cache) {
3651
3754
  const index = buildPricingIndex(cache);
3652
3755
  let changed = false;
3653
3756
  for (const provider of registry.providers) {
3654
3757
  if (!provider.modelsCache?.models.length) continue;
3655
- const platform = TEMPLATE_TO_PRICING_PLATFORM[provider.templateId] ?? TEMPLATE_TO_PRICING_PLATFORM[provider.id];
3656
- const enriched = enrichModelsWithPricing(provider.modelsCache.models, index, platform);
3758
+ const enriched = enrichModelsForProviderPricing(
3759
+ provider.modelsCache.models,
3760
+ index,
3761
+ provider.templateId,
3762
+ provider.id
3763
+ );
3657
3764
  if (JSON.stringify(enriched) !== JSON.stringify(provider.modelsCache.models)) {
3658
3765
  provider.modelsCache = { ...provider.modelsCache, models: enriched };
3659
3766
  changed = true;
@@ -3699,6 +3806,8 @@ var REGISTRY_TO_MODELS_DEV = {
3699
3806
  perplexity: "perplexity",
3700
3807
  cohere: "cohere",
3701
3808
  alibaba: "alibaba",
3809
+ "qwen-cloud-token-plan": "alibaba-token-plan",
3810
+ "qwen-cloud-payg": "alibaba",
3702
3811
  openrouter: "openrouter",
3703
3812
  anthropic: "anthropic",
3704
3813
  nvidia: "nvidia",
@@ -6781,6 +6890,106 @@ function materializeRegistry(registry, resolveCredential, opts) {
6781
6890
  return result;
6782
6891
  }
6783
6892
 
6893
+ // src/registry/copilot-models.ts
6894
+ var FREE_MODEL_IDS = /* @__PURE__ */ new Set([
6895
+ "gpt-4.1",
6896
+ "gpt-4o",
6897
+ "gpt-4o-mini",
6898
+ "raptor-mini",
6899
+ "goldeneye"
6900
+ ]);
6901
+ var FREE_CHAT_BLOCKLIST = /* @__PURE__ */ new Set(["gpt-5-mini"]);
6902
+ function copilotPlanTier(providerData) {
6903
+ const copilot = providerData?.["copilot"];
6904
+ if (!copilot || typeof copilot !== "object" || Array.isArray(copilot)) return "unknown";
6905
+ const summary = copilot;
6906
+ if (summary["lookup_status"] === "unknown") return "unknown";
6907
+ if (summary["is_free_plan"] === true) return "free";
6908
+ if (summary["is_free_plan"] === false) return "paid";
6909
+ return "unknown";
6910
+ }
6911
+ function normalizeCopilotModels(rows, tier) {
6912
+ const models = [];
6913
+ for (const value of rows) {
6914
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
6915
+ const row = value;
6916
+ const id = typeof row["id"] === "string" ? row["id"].trim() : "";
6917
+ if (!id || !copilotModelAllowed(row, tier)) continue;
6918
+ const lowerId = id.toLowerCase();
6919
+ const isFree = tier !== "paid" || copilotModelIsIncluded(row);
6920
+ const family = lowerId.split(/[-/:]/)[0] ?? lowerId;
6921
+ const contextWindow = numericValue(row["context_length"]) ?? numericValue(row["contextWindow"]) ?? numericValue(row["context_window"]) ?? resolveContextWindow(id);
6922
+ models.push({
6923
+ id,
6924
+ name: `${id} [Copilot]`,
6925
+ upstreamModelId: id,
6926
+ family,
6927
+ brand: deriveBrand(family),
6928
+ contextWindow,
6929
+ isFree,
6930
+ freeStatus: isFree ? "verified_free" : "unknown",
6931
+ modelFormat: "openai",
6932
+ npm: "@ai-sdk/openai-compatible",
6933
+ apiUrl: "https://api.githubcopilot.com"
6934
+ });
6935
+ }
6936
+ return models;
6937
+ }
6938
+ function filterCachedCopilotModels(models, tier) {
6939
+ return models.flatMap((model) => {
6940
+ const id = model.id.toLowerCase();
6941
+ if (!copilotIdIsCallable(id)) return [];
6942
+ if (tier !== "paid" && (!FREE_MODEL_IDS.has(id) || FREE_CHAT_BLOCKLIST.has(id))) return [];
6943
+ if (tier === "paid") return [model];
6944
+ return [{ ...model, isFree: true, freeStatus: "verified_free" }];
6945
+ });
6946
+ }
6947
+ function copilotModelAllowed(row, tier) {
6948
+ const id = String(row["id"] ?? "").toLowerCase();
6949
+ if (!copilotIdIsCallable(id)) return false;
6950
+ if (row["model_picker_enabled"] === false) return false;
6951
+ const policy = row["policy"];
6952
+ if (policy && typeof policy === "object" && !Array.isArray(policy)) {
6953
+ if (String(policy["state"] ?? "").toLowerCase() === "disabled") return false;
6954
+ }
6955
+ const capabilities = row["capabilities"];
6956
+ if (capabilities && typeof capabilities === "object" && !Array.isArray(capabilities)) {
6957
+ const family = String(capabilities["family"] ?? "").toLowerCase();
6958
+ if (family.includes("embedding")) return false;
6959
+ }
6960
+ const endpoints = row["supported_endpoints"];
6961
+ if (Array.isArray(endpoints) && endpoints.length > 0) {
6962
+ const supportsChat = endpoints.some((endpoint) => {
6963
+ const normalized = String(endpoint).toLowerCase().replace(/\/$/, "");
6964
+ return normalized.endsWith("/chat/completions") || normalized === "chat/completions";
6965
+ });
6966
+ if (!supportsChat) return false;
6967
+ }
6968
+ if (tier !== "paid" && (!FREE_MODEL_IDS.has(id) || FREE_CHAT_BLOCKLIST.has(id))) return false;
6969
+ return true;
6970
+ }
6971
+ function copilotIdIsCallable(id) {
6972
+ return id !== "auto" && !id.endsWith("-auto") && !id.includes("embedding");
6973
+ }
6974
+ function copilotModelIsIncluded(row) {
6975
+ const id = String(row["id"] ?? "").toLowerCase();
6976
+ if (FREE_CHAT_BLOCKLIST.has(id) || !copilotIdIsCallable(id)) return false;
6977
+ const billing = row["billing"];
6978
+ if (billing && typeof billing === "object" && !Array.isArray(billing)) {
6979
+ const multiplier = numericValue(billing["multiplier"]);
6980
+ if (multiplier !== void 0) return multiplier === 0;
6981
+ }
6982
+ return FREE_MODEL_IDS.has(id);
6983
+ }
6984
+ function numericValue(value) {
6985
+ if (typeof value === "number" && Number.isFinite(value)) return value;
6986
+ if (typeof value === "string" && value.trim()) {
6987
+ const number = Number(value);
6988
+ if (Number.isFinite(number)) return number;
6989
+ }
6990
+ return void 0;
6991
+ }
6992
+
6784
6993
  // src/registry/load.ts
6785
6994
  async function loadRegistryProviders(diag, opts) {
6786
6995
  const registry = loadRegistry();
@@ -6804,7 +7013,23 @@ async function loadRegistryProviders(diag, opts) {
6804
7013
  }
6805
7014
  }
6806
7015
  }));
6807
- return materializeRegistry(registry, (provider) => keys.get(provider.id) ?? null, opts).map((provider) => ({
7016
+ const runtimeRegistry = {
7017
+ ...registry,
7018
+ providers: registry.providers.map((provider) => {
7019
+ if (provider.id !== "github-copilot" || !provider.modelsCache) return provider;
7020
+ return {
7021
+ ...provider,
7022
+ modelsCache: {
7023
+ ...provider.modelsCache,
7024
+ models: filterCachedCopilotModels(
7025
+ provider.modelsCache.models,
7026
+ copilotPlanTier(oauthProviderData.get(provider.id))
7027
+ )
7028
+ }
7029
+ };
7030
+ })
7031
+ };
7032
+ return materializeRegistry(runtimeRegistry, (provider) => keys.get(provider.id) ?? null, opts).map((provider) => ({
6808
7033
  ...provider,
6809
7034
  oauthAccountId: oauthAccountIds.get(provider.id),
6810
7035
  providerData: oauthProviderData.get(provider.id)
@@ -8992,11 +9217,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
8992
9217
  }
8993
9218
  const now = (/* @__PURE__ */ new Date()).toISOString();
8994
9219
  const pricingCache = loadPricingCache();
8995
- const platform = pricingPlatformForProvider(template.id, template.id);
8996
- const pricedModels = enrichModelsWithPricing(
9220
+ const pricedModels = enrichModelsForProviderPricing(
8997
9221
  usableModels.map((m) => ({ ...m, apiUrl: fetched.baseUrl })),
8998
9222
  buildPricingIndex(pricingCache),
8999
- platform
9223
+ template.id,
9224
+ template.id
9000
9225
  );
9001
9226
  const entry = {
9002
9227
  id: template.id,
@@ -9308,10 +9533,49 @@ async function refreshAntigravityOAuthModels(accessToken) {
9308
9533
  }
9309
9534
  throw new Error("Antigravity live model refresh failed \u2014 Cloud Code returned no usable models");
9310
9535
  }
9536
+ function buildCopilotFreeFallback() {
9537
+ return normalizeCopilotModels([
9538
+ {
9539
+ id: "gpt-4.1",
9540
+ supported_endpoints: ["/chat/completions"],
9541
+ billing: { multiplier: 0 }
9542
+ },
9543
+ {
9544
+ id: "gpt-4o",
9545
+ supported_endpoints: ["/chat/completions"],
9546
+ billing: { multiplier: 0 }
9547
+ }
9548
+ ], "free");
9549
+ }
9550
+ async function refreshGithubCopilotOAuthModels(accessToken, tier) {
9551
+ const result = await fetchJsonWithAuth(
9552
+ "https://api.githubcopilot.com/models",
9553
+ accessToken,
9554
+ 1e4,
9555
+ { "Editor-Version": "vscode/1.85.1" }
9556
+ );
9557
+ const body = result.body;
9558
+ const rows = Array.isArray(body) ? body : body && typeof body === "object" ? Array.isArray(body["data"]) ? body["data"] : Array.isArray(body["models"]) ? body["models"] : [] : [];
9559
+ const models = normalizeCopilotModels(rows, tier);
9560
+ if (models.length > 0) return { models, source: "live" };
9561
+ return {
9562
+ models: buildCopilotFreeFallback(),
9563
+ source: "seed",
9564
+ failureReason: result.error ?? "GitHub Copilot returned no usable chat models",
9565
+ replaceCacheOnFallback: tier !== "paid"
9566
+ };
9567
+ }
9311
9568
  async function refreshOAuthProvider(provider, accessToken) {
9312
9569
  const tpl = provider.templateId ?? provider.id;
9313
9570
  if (tpl === "openai" || tpl === "openai-oauth") return refreshOpenAiOAuthModels(accessToken);
9314
9571
  if (tpl === "xai" || tpl === "xai-oauth") return refreshXaiOAuthModels(accessToken);
9572
+ if (tpl === "github-copilot") {
9573
+ let providerData = await resolveProviderOAuthProviderData(provider.authRef);
9574
+ if (copilotPlanTier(providerData) === "unknown") {
9575
+ providerData = await enrichGithubCopilotOAuthProviderData(provider.authRef) ?? providerData;
9576
+ }
9577
+ return refreshGithubCopilotOAuthModels(accessToken, copilotPlanTier(providerData));
9578
+ }
9315
9579
  if (tpl === "claude-code") return refreshClaudeCodeOAuthModels(accessToken);
9316
9580
  if (tpl === "antigravity") return refreshAntigravityOAuthModels(accessToken);
9317
9581
  throw new Error(`refreshOAuthProvider: unsupported template "${tpl}"`);
@@ -9369,7 +9633,7 @@ function buildDynamicOAuthModel(entry, seedById) {
9369
9633
  preferWebSockets: entry.preferWebSockets
9370
9634
  };
9371
9635
  }
9372
- async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
9636
+ async function fetchJsonWithAuth(url, accessToken, timeoutMs, extraHeaders = {}) {
9373
9637
  try {
9374
9638
  const controller = new AbortController();
9375
9639
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -9377,7 +9641,8 @@ async function fetchJsonWithAuth(url, accessToken, timeoutMs) {
9377
9641
  headers: {
9378
9642
  Accept: "application/json",
9379
9643
  Authorization: `Bearer ${accessToken}`,
9380
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
9644
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
9645
+ ...extraHeaders
9381
9646
  },
9382
9647
  signal: controller.signal
9383
9648
  }).finally(() => clearTimeout(timer));
@@ -9533,7 +9798,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
9533
9798
  let oauthFallbackReason;
9534
9799
  if (source === "zen-go-api") {
9535
9800
  models = await refreshZenGoProvider(provider);
9536
- } else if (provider.authType === "oauth" && (["openai", "xai", "xai-oauth", "claude-code", "antigravity"].includes(provider.templateId ?? provider.id) || provider.id === "openai-oauth" || provider.id === "xai-oauth")) {
9801
+ } else if (provider.authType === "oauth" && (["openai", "xai", "xai-oauth", "github-copilot", "claude-code", "antigravity"].includes(provider.templateId ?? provider.id) || provider.id === "openai-oauth" || provider.id === "xai-oauth")) {
9537
9802
  if (!apiKey) {
9538
9803
  return {
9539
9804
  id: provider.id,
@@ -9544,7 +9809,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
9544
9809
  }
9545
9810
  const oauthResult = await refreshOAuthProvider(provider, apiKey);
9546
9811
  const failureDetail = oauthResult.failureReason ? ` (${oauthResult.failureReason})` : "";
9547
- if (oauthResult.source === "seed" && cachedModelCount(provider) > 0) {
9812
+ if (oauthResult.source === "seed" && cachedModelCount(provider) > 0 && !oauthResult.replaceCacheOnFallback) {
9548
9813
  return skipWithCachedModels(
9549
9814
  provider,
9550
9815
  `Live model discovery failed${failureDetail} \u2014 kept your existing cached model list instead of overwriting it with relay-ai's built-in fallback list. Try refreshing again later.`
@@ -9602,10 +9867,14 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
9602
9867
  baseUrl = fetched.baseUrl;
9603
9868
  }
9604
9869
  const pricingCache = loadPricingCache();
9605
- const platform = pricingPlatformForProvider(provider.templateId, provider.id);
9606
9870
  const enriched = compatibleCachedModels(
9607
9871
  provider,
9608
- enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform)
9872
+ enrichModelsForProviderPricing(
9873
+ models,
9874
+ buildPricingIndex(pricingCache),
9875
+ provider.templateId,
9876
+ provider.id
9877
+ )
9609
9878
  );
9610
9879
  if (provider.id === "antigravity" && enriched.length === 0) {
9611
9880
  return {
@@ -9766,7 +10035,7 @@ var PROVIDER_DISPLAY = {
9766
10035
  "xai-oauth": "xAI Grok (SuperGrok)",
9767
10036
  openai: OPENAI_DISPLAY,
9768
10037
  "openai-oauth": OPENAI_DISPLAY,
9769
- "github-copilot": "GitHub Copilot (Individual / Business)",
10038
+ "github-copilot": "GitHub Copilot",
9770
10039
  "claude-code": "Claude Code (Anthropic subscription)",
9771
10040
  antigravity: "Antigravity (Google Cloud Code Assist)"
9772
10041
  };
@@ -10019,7 +10288,7 @@ function providerAuthHelpText() {
10019
10288
 
10020
10289
  ${pc6.bold("Usage:")}
10021
10290
  relay-ai providers auth <id>
10022
- relay-ai providers auth xai --native
10291
+ relay-ai providers auth xai-oauth --native
10023
10292
  relay-ai providers auth openai --broker
10024
10293
  relay-ai providers auth github-copilot
10025
10294
 
@@ -10028,9 +10297,9 @@ ${pc6.bold("Options:")}
10028
10297
  --broker Delegate to OpenCode auth login
10029
10298
 
10030
10299
  ${pc6.bold("Device code (works on SSH/VPS):")}
10031
- xai SuperGrok / X Premium (device code at x.ai/device)
10032
- openai ChatGPT Plus/Pro (device code at auth.openai.com/codex/device)
10033
- github-copilot GitHub Copilot Individual/Business (device code at github.com/login/device)`;
10300
+ xai-oauth SuperGrok / X Premium (device code at x.ai/device)
10301
+ openai-oauth ChatGPT Plus/Pro (device code at auth.openai.com/codex/device)
10302
+ github-copilot GitHub Copilot Free or paid (device code at github.com/login/device)`;
10034
10303
  }
10035
10304
 
10036
10305
  // src/codex/app-launch.ts
@@ -10618,6 +10887,7 @@ export {
10618
10887
  makeRouteResolver,
10619
10888
  buildCatalogRoutes,
10620
10889
  cachedModelToLocal,
10890
+ copilotPlanTier,
10621
10891
  fetchProviderCatalog,
10622
10892
  providersForPicker,
10623
10893
  resolveLocalProviderApiKey,
@@ -10665,4 +10935,4 @@ export {
10665
10935
  supportsClaudeTransparentMode,
10666
10936
  buildHttpProxyRoutes
10667
10937
  };
10668
- //# sourceMappingURL=chunk-I3SSHXSP.js.map
10938
+ //# sourceMappingURL=chunk-44KQK6Y5.js.map