diff --git a/package.json b/package.json index 097e137..513a989 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "prebuild:renderer": "npm run gen:catalog", "catalog:check": "node scripts/check-catalog-stale.mjs", "test": "npm run build:main && npm run test:unit", - "test:unit": "node scripts/test-extract-base-url.mjs && node scripts/test-doctor.mjs && node scripts/test-versions.mjs && node scripts/test-secrets.mjs && node scripts/test-homebrew.mjs && node scripts/test-install-methods.mjs && node scripts/test-codex-auth.mjs", + "test:unit": "node scripts/test-extract-base-url.mjs && node scripts/test-doctor.mjs && node scripts/test-versions.mjs && node scripts/test-secrets.mjs && node scripts/test-homebrew.mjs && node scripts/test-install-methods.mjs && node scripts/test-codex-auth.mjs && node scripts/test-probes-cloud.mjs", "test:extractor": "node scripts/test-extract-base-url.mjs", "package": "npm run build && electron-builder" }, diff --git a/scripts/test-probes-cloud.mjs b/scripts/test-probes-cloud.mjs new file mode 100644 index 0000000..550e956 --- /dev/null +++ b/scripts/test-probes-cloud.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env node +/** + * Unit tests for src/main/probes/{aws,gcp,azure}.ts (via dist build). + * All HTTP is mocked with a local fixture server. No live provider keys. + */ +import { createRequire } from 'node:module' +import { createServer } from 'node:http' +import { existsSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = join(dirname(fileURLToPath(import.meta.url)), '..') +for (const rel of ['dist/main/probes/aws.js', 'dist/main/probes/gcp.js', 'dist/main/probes/azure.js', 'dist/main/probes/index.js']) { + if (!existsSync(join(root, rel))) { + console.error(`${rel} missing - run npm run build:main first`) + process.exit(1) + } +} + +const require = createRequire(import.meta.url) +const { probeBedrock, signBedrockRequest, parseAwsError } = require(join(root, 'dist/main/probes/aws.js')) +const { probeVertex, vertexModelsUrl } = require(join(root, 'dist/main/probes/gcp.js')) +const { probeAzure } = require(join(root, 'dist/main/probes/azure.js')) +const { runProbe } = require(join(root, 'dist/main/probes/index.js')) + +let failed = 0 +function assert(cond, msg) { + if (cond) console.log(` ✓ ${msg}`) + else { + failed++ + console.error(` ✗ ${msg}`) + } +} + +// Deterministic fixture server: handler swapped per case, requests recorded. +let handler = (_req, res) => { res.writeHead(200); res.end('{}') } +const requests = [] +const server = createServer((req, res) => { + requests.push({ method: req.method, url: req.url, headers: req.headers }) + handler(req, res) +}) +await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) +const endpoint = `http://127.0.0.1:${server.address().port}` +const lastRequest = () => requests[requests.length - 1] + +// Keep the AWS env chain empty unless a case sets it explicitly. +const savedEnv = { ...process.env } +for (const key of ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION']) { + delete process.env[key] +} + +const fakeCreds = { accessKeyId: 'AKIDEXAMPLE', secretAccessKey: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' } + +// AWS: unconfigured credential chain. +{ + const r = await probeBedrock({ endpoint }) + assert(r.valid === false && r.status === 'invalid', 'aws no credentials returns invalid') + assert(r.detail.includes('AWS error:'), 'aws no credentials detail carries AWS error') +} + +// AWS: signed request validates against a mocked 200. +{ + handler = (_req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"modelSummaries":[]}') } + const before = requests.length + const r = await probeBedrock({ credentials: fakeCreds, region: 'us-east-1', endpoint }) + assert(r.valid === true && r.status === 'ok', 'aws mocked 200 returns ok') + assert(requests.length === before + 1, 'aws probe hit the fixture server once') + const req = lastRequest() + assert(req.url === '/foundation-models', 'aws request path is /foundation-models') + assert(req.headers['x-amz-date'], 'aws request carries x-amz-date') + const auth = req.headers.authorization ?? '' + assert(auth.startsWith('AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/'), 'aws Authorization uses SigV4 credential scope') + assert(auth.includes('/us-east-1/bedrock/aws4_request'), 'aws credential scope names region and bedrock service') + assert(/Signature=[0-9a-f]{64}/.test(auth), 'aws Authorization carries a hex signature') +} + +// AWS: mocked 403 XML error surfaces the AWS error code. +{ + handler = (_req, res) => { + res.writeHead(403, { 'content-type': 'application/xml' }) + res.end('AccessDeniedExceptionnot entitled') + } + const r = await probeBedrock({ credentials: fakeCreds, region: 'us-east-1', endpoint }) + assert(r.valid === false && r.status === 'invalid', 'aws mocked 403 returns invalid') + assert(r.detail.includes('AWS error: AccessDeniedException'), 'aws detail surfaces the AWS error code') + assert(r.detail.includes('not entitled'), 'aws detail surfaces the AWS error message') +} + +// AWS: signing is deterministic for a fixed clock and secret-sensitive. +{ + const now = new Date('2026-01-02T03:04:05Z') + const a = signBedrockRequest({ endpoint: 'https://bedrock.us-east-1.amazonaws.com', region: 'us-east-1', credentials: fakeCreds, now }) + const b = signBedrockRequest({ endpoint: 'https://bedrock.us-east-1.amazonaws.com', region: 'us-east-1', credentials: fakeCreds, now }) + assert(a.headers.Authorization === b.headers.Authorization, 'aws SigV4 signature is deterministic for a fixed clock') + const c = signBedrockRequest({ endpoint: 'https://bedrock.us-east-1.amazonaws.com', region: 'us-east-1', credentials: { ...fakeCreds, secretAccessKey: 'other-secret' }, now }) + assert(c.headers.Authorization !== a.headers.Authorization, 'aws SigV4 signature changes with the secret') + const parsed = parseAwsError('ValidationExceptionbad') + assert(parsed.code === 'ValidationException' && parsed.message === 'bad', 'parseAwsError reads XML code and message') +} + +// GCP: project and region are required before any HTTP call. +{ + const before = requests.length + const r = await probeVertex({ apiKey: 'ya29.fake-token' }) + assert(r.valid === false && r.status === 'error', 'gcp missing project and region returns error') + assert(r.detail.includes('project and region'), 'gcp missing config detail names project and region') + assert(requests.length === before, 'gcp missing config makes no HTTP request') + const noToken = await probeVertex({ apiKey: '', project: 'my-project', region: 'us-central1', endpoint }) + assert(noToken.valid === false && noToken.status === 'invalid', 'gcp missing token returns invalid') +} + +// GCP: mocked 200 validates, mocked 403 surfaces the Google error. +{ + handler = (_req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"models":[]}') } + const r = await probeVertex({ apiKey: 'ya29.fake-token', project: 'my-project', region: 'us-central1', endpoint }) + assert(r.valid === true && r.status === 'ok', 'gcp mocked 200 returns ok') + const req = lastRequest() + assert(req.url === '/v1/projects/my-project/locations/us-central1/publishers/google/models', 'gcp request path carries project and region') + assert(req.headers.authorization === 'Bearer ya29.fake-token', 'gcp request sends the Bearer token') + + handler = (_req, res) => { + res.writeHead(403, { 'content-type': 'application/json' }) + res.end('{"error":{"status":"PERMISSION_DENIED","message":"Permission denied"}}') + } + const denied = await probeVertex({ apiKey: 'ya29.fake-token', project: 'my-project', region: 'us-central1', endpoint }) + assert(denied.valid === false && denied.status === 'invalid', 'gcp mocked 403 returns invalid') + assert(denied.detail.includes('PERMISSION_DENIED'), 'gcp detail surfaces the Google error status') + assert( + vertexModelsUrl('p', 'global') === 'https://aiplatform.googleapis.com/v1/projects/p/locations/global/publishers/google/models', + 'gcp global region uses the global aiplatform host', + ) +} + +// Azure: endpoint required, mocked 200 validates with the api-key header. +{ + const noEndpoint = await probeAzure({ apiKey: 'fake-key' }) + assert(noEndpoint.valid === false && noEndpoint.status === 'error', 'azure missing endpoint returns error') + assert(noEndpoint.detail.includes('endpoint'), 'azure missing endpoint detail names the endpoint') + + handler = (_req, res) => { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"data":[]}') } + const r = await probeAzure({ apiKey: 'fake-key', baseUrl: endpoint }) + assert(r.valid === true && r.status === 'ok', 'azure mocked 200 returns ok') + const req = lastRequest() + assert(req.url === '/openai/deployments?api-version=2024-02-01', 'azure request path lists deployments') + assert(req.headers['api-key'] === 'fake-key', 'azure request sends the api-key header') + + handler = (_req, res) => { res.writeHead(401); res.end('{}') } + const denied = await probeAzure({ apiKey: 'fake-key', baseUrl: endpoint }) + assert(denied.valid === false && denied.status === 'invalid', 'azure mocked 401 returns invalid') +} + +// runProbe dispatch: cloud kinds no longer fall through to No probe implemented. +{ + const bedrock = await runProbe({ providerId: 'bedrock', apiKey: '' }) + assert(bedrock.detail.includes('AWS error:'), 'runProbe dispatches bedrock to the aws probe') + const vertex = await runProbe({ providerId: 'vertex', apiKey: 'ya29.fake-token' }) + assert(vertex.detail.includes('project and region'), 'runProbe dispatches vertex to the gcp probe') + const azure = await runProbe({ providerId: 'azure-openai', apiKey: 'fake-key' }) + assert(azure.detail.includes('endpoint'), 'runProbe dispatches azure-openai to the azure probe') + const unknown = await runProbe({ providerId: 'no-such-provider', apiKey: 'x' }) + assert(unknown.detail.includes('No probe implemented'), 'runProbe still reports unimplemented providers') +} + +Object.assign(process.env, savedEnv) +server.close() +if (failed > 0) { + console.error(`\n${failed} cloud probe case(s) failed`) + process.exit(1) +} +console.log('\nall cloud probe cases passed') diff --git a/src/main/probes/aws.ts b/src/main/probes/aws.ts new file mode 100644 index 0000000..761bf4d --- /dev/null +++ b/src/main/probes/aws.ts @@ -0,0 +1,147 @@ +import { createHash, createHmac } from 'node:crypto' +import type { ProbeResult } from './types' + +const TIMEOUT_MS = 5000 +const SERVICE = 'bedrock' +const DEFAULT_REGION = 'us-east-1' + +export interface AwsCredentials { + accessKeyId: string + secretAccessKey: string + sessionToken?: string +} + +export interface BedrockProbeOptions { + /** Explicit credentials. Falls back to the AWS env chain when omitted. */ + credentials?: AwsCredentials + /** Region override. Falls back to AWS_REGION / AWS_DEFAULT_REGION / us-east-1. */ + region?: string + /** Endpoint override (tests). Defaults to the Bedrock control plane for the region. */ + endpoint?: string + /** Clock override (tests) so the SigV4 signature is deterministic. */ + now?: Date +} + +function credentialsFromEnv(): AwsCredentials | null { + const accessKeyId = process.env.AWS_ACCESS_KEY_ID + const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY + if (!accessKeyId || !secretAccessKey) return null + return { + accessKeyId, + secretAccessKey, + sessionToken: process.env.AWS_SESSION_TOKEN, + } +} + +function regionFromEnv(): string { + return process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? DEFAULT_REGION +} + +function sha256Hex(value: string): string { + return createHash('sha256').update(value, 'utf8').digest('hex') +} + +function hmac(key: Buffer | string, value: string): Buffer { + return createHmac('sha256', key).update(value, 'utf8').digest() +} + +/** Sign a GET request with AWS Signature Version 4 for the bedrock service. */ +export function signBedrockRequest(opts: { + endpoint: string + region: string + credentials: AwsCredentials + now: Date +}): { url: string; headers: Record } { + const url = new URL('/foundation-models', opts.endpoint) + const host = url.host + const amzDate = opts.now.toISOString().replace(/[:-]|\.\d{3}/g, '') + const dateStamp = amzDate.slice(0, 8) + + const headers: Record = { + host, + 'x-amz-date': amzDate, + } + if (opts.credentials.sessionToken) { + headers['x-amz-security-token'] = opts.credentials.sessionToken + } + const signedHeaderNames = Object.keys(headers).sort() + const canonicalHeaders = signedHeaderNames.map((name) => `${name}:${headers[name]}\n`).join('') + const signedHeaders = signedHeaderNames.join(';') + const payloadHash = sha256Hex('') + + const canonicalRequest = ['GET', url.pathname, '', canonicalHeaders, signedHeaders, payloadHash].join('\n') + const scope = `${dateStamp}/${opts.region}/${SERVICE}/aws4_request` + const stringToSign = ['AWS4-HMAC-SHA256', amzDate, scope, sha256Hex(canonicalRequest)].join('\n') + + const kDate = hmac(`AWS4${opts.credentials.secretAccessKey}`, dateStamp) + const kRegion = hmac(kDate, opts.region) + const kService = hmac(kRegion, SERVICE) + const kSigning = hmac(kService, 'aws4_request') + const signature = createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex') + + headers.Authorization = + `AWS4-HMAC-SHA256 Credential=${opts.credentials.accessKeyId}/${scope}, ` + + `SignedHeaders=${signedHeaders}, Signature=${signature}` + + return { url: url.toString(), headers } +} + +/** Extract the AWS error code and message from an XML or JSON error body. */ +export function parseAwsError(body: string): { code?: string; message?: string } { + const code = body.match(/([^<]+)<\/Code>/)?.[1] ?? body.match(/"__type"\s*:\s*"[^"#]*#([^"]+)"/)?.[1] + const message = + body.match(/([^<]+)<\/Message>/)?.[1] ?? + body.match(/"message"\s*:\s*"([^"]+)"/i)?.[1] + return { code, message } +} + +export async function probeBedrock(opts: BedrockProbeOptions = {}): Promise { + const checkedAt = new Date().toISOString() + const credentials = opts.credentials ?? credentialsFromEnv() + + if (!credentials) { + return { + valid: false, + status: 'invalid', + detail: + 'AWS error: NoCredentialProviders: no AWS credentials found. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or configure the standard AWS credential chain.', + checkedAt, + } + } + + const region = opts.region ?? regionFromEnv() + const endpoint = opts.endpoint ?? `https://bedrock.${region}.amazonaws.com` + const { url, headers } = signBedrockRequest({ + endpoint, + region, + credentials, + now: opts.now ?? new Date(), + }) + + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + try { + const res = await fetch(url, { method: 'GET', headers, signal: controller.signal }) + if (res.ok) { + return { valid: true, status: 'ok', detail: 'Credentials validated against Bedrock ListFoundationModels.', checkedAt } + } + const body = await res.text().catch(() => '') + const { code, message } = parseAwsError(body) + const awsDetail = code ? `AWS error: ${code}${message ? `: ${message}` : ''}` : `AWS error: Unexpected HTTP ${res.status}.` + if (res.status === 429 || code === 'ThrottlingException') { + return { valid: true, status: 'quota_exceeded', detail: awsDetail, checkedAt } + } + if (res.status === 401 || res.status === 403) { + return { valid: false, status: 'invalid', detail: awsDetail, checkedAt } + } + return { valid: false, status: 'error', detail: awsDetail, checkedAt } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + if (message.includes('abort')) { + return { valid: false, status: 'error', detail: `Timed out after ${TIMEOUT_MS}ms.`, checkedAt } + } + return { valid: false, status: 'error', detail: `AWS error: ${message}`, checkedAt } + } finally { + clearTimeout(timer) + } +} diff --git a/src/main/probes/azure.ts b/src/main/probes/azure.ts new file mode 100644 index 0000000..cbd81f6 --- /dev/null +++ b/src/main/probes/azure.ts @@ -0,0 +1,59 @@ +import type { ProbeResult } from './types' + +const TIMEOUT_MS = 5000 +const DEFAULT_API_VERSION = '2024-02-01' + +export interface AzureProbeOptions { + apiKey: string + /** Azure OpenAI resource endpoint, for example https://.openai.azure.com */ + baseUrl?: string + apiVersion?: string +} + +export async function probeAzure(opts: AzureProbeOptions): Promise { + const checkedAt = new Date().toISOString() + + if (!opts.baseUrl) { + return { + valid: false, + status: 'error', + detail: 'Azure probe requires an endpoint. Set the Azure OpenAI endpoint (AZURE_OPENAI_ENDPOINT) for this key first.', + checkedAt, + } + } + + if (!opts.apiKey) { + return { valid: false, status: 'invalid', detail: 'No API key supplied.', checkedAt } + } + + const apiVersion = opts.apiVersion ?? DEFAULT_API_VERSION + const url = `${opts.baseUrl.replace(/\/$/, '')}/openai/deployments?api-version=${apiVersion}` + + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + try { + const res = await fetch(url, { + method: 'GET', + headers: { 'api-key': opts.apiKey }, + signal: controller.signal, + }) + if (res.ok) { + return { valid: true, status: 'ok', detail: 'Key validated against Azure OpenAI deployments.', checkedAt } + } + if (res.status === 401 || res.status === 403) { + return { valid: false, status: 'invalid', detail: `Authentication failed (${res.status}).`, checkedAt } + } + if (res.status === 429) { + return { valid: true, status: 'quota_exceeded', detail: 'Rate limited; credential accepted.', checkedAt } + } + return { valid: false, status: 'error', detail: `Unexpected HTTP ${res.status}.`, checkedAt } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + if (message.includes('abort')) { + return { valid: false, status: 'error', detail: `Timed out after ${TIMEOUT_MS}ms.`, checkedAt } + } + return { valid: false, status: 'error', detail: message, checkedAt } + } finally { + clearTimeout(timer) + } +} diff --git a/src/main/probes/gcp.ts b/src/main/probes/gcp.ts new file mode 100644 index 0000000..dcb5a79 --- /dev/null +++ b/src/main/probes/gcp.ts @@ -0,0 +1,99 @@ +import type { ProbeResult } from './types' + +const TIMEOUT_MS = 5000 +const API_VERSION_PATH = 'publishers/google/models' + +export interface VertexProbeOptions { + /** OAuth2 access token (for example from ADC via gcloud) sent as Bearer. */ + apiKey: string + /** GCP project id. Required; the probe errors without it. */ + project?: string + /** Vertex region, for example us-central1 or global. Required. */ + region?: string + /** Endpoint override (tests). Defaults to the regional aiplatform host. */ + endpoint?: string +} + +export function vertexModelsUrl(project: string, region: string, endpoint?: string): string { + const base = + endpoint ?? + (region === 'global' + ? 'https://aiplatform.googleapis.com' + : `https://${region}-aiplatform.googleapis.com`) + return `${base.replace(/\/$/, '')}/v1/projects/${project}/locations/${region}/${API_VERSION_PATH}` +} + +function parseGoogleError(body: string): string | undefined { + try { + const parsed = JSON.parse(body) as { error?: { status?: string; message?: string } } + if (parsed.error?.message) { + return parsed.error.status ? `${parsed.error.status}: ${parsed.error.message}` : parsed.error.message + } + } catch { + // fall through to the raw body snippet + } + return body.trim() ? body.trim().slice(0, 200) : undefined +} + +export async function probeVertex(opts: VertexProbeOptions): Promise { + const checkedAt = new Date().toISOString() + + if (!opts.project || !opts.region) { + return { + valid: false, + status: 'error', + detail: 'Vertex probe requires a GCP project and region. Save a project id and region for this key first.', + checkedAt, + } + } + + if (!opts.apiKey) { + return { + valid: false, + status: 'invalid', + detail: 'No access token supplied. Run `gcloud auth application-default login` and try again.', + checkedAt, + } + } + + const url = vertexModelsUrl(opts.project, opts.region, opts.endpoint) + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + try { + const res = await fetch(url, { + method: 'GET', + headers: { Authorization: `Bearer ${opts.apiKey}` }, + signal: controller.signal, + }) + if (res.ok) { + return { valid: true, status: 'ok', detail: 'Credentials validated against Vertex AI publishers/google/models.', checkedAt } + } + const body = await res.text().catch(() => '') + const googleDetail = parseGoogleError(body) + if (res.status === 401 || res.status === 403) { + return { + valid: false, + status: 'invalid', + detail: googleDetail ? `Vertex error: ${googleDetail}` : `Authentication failed (${res.status}).`, + checkedAt, + } + } + if (res.status === 429) { + return { valid: true, status: 'quota_exceeded', detail: 'Vertex rate limited; credential accepted.', checkedAt } + } + return { + valid: false, + status: 'error', + detail: googleDetail ? `Vertex error: ${googleDetail}` : `Unexpected HTTP ${res.status}.`, + checkedAt, + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + if (message.includes('abort')) { + return { valid: false, status: 'error', detail: `Timed out after ${TIMEOUT_MS}ms.`, checkedAt } + } + return { valid: false, status: 'error', detail: message, checkedAt } + } finally { + clearTimeout(timer) + } +} diff --git a/src/main/probes/index.ts b/src/main/probes/index.ts index ad83e10..6ca3422 100644 --- a/src/main/probes/index.ts +++ b/src/main/probes/index.ts @@ -1,17 +1,30 @@ import { probeAnthropic } from './anthropic' import { probeOpenAI } from './openai' +import { probeBedrock } from './aws' +import { probeVertex } from './gcp' +import { probeAzure } from './azure' import type { ProbeResult } from './types' import { PROVIDER_CATALOG } from '../providers/catalog.generated' export type { ProbeResult } from './types' export type { AnthropicProbeOptions } from './anthropic' +export type { BedrockProbeOptions, AwsCredentials } from './aws' +export type { VertexProbeOptions } from './gcp' +export type { AzureProbeOptions } from './azure' export { probeAnthropic } from './anthropic' export { probeOpenAI } from './openai' +export { probeBedrock } from './aws' +export { probeVertex } from './gcp' +export { probeAzure } from './azure' export interface ProbeContext { providerId: string apiKey: string baseUrl?: string + /** GCP project id for the Vertex (gcp) probe. */ + project?: string + /** Cloud region for the Vertex (gcp) and Bedrock (aws) probes. */ + region?: string } /** Providers that share the OpenAI-compatible /models probe. */ @@ -46,6 +59,25 @@ export async function runProbe(ctx: ProbeContext): Promise { }) } + if (kind === 'aws') { + return probeBedrock({ region: ctx.region }) + } + + if (kind === 'gcp') { + return probeVertex({ + apiKey: ctx.apiKey, + project: ctx.project, + region: ctx.region, + }) + } + + if (kind === 'azure') { + return probeAzure({ + apiKey: ctx.apiKey, + baseUrl: ctx.baseUrl ?? provider?.defaultBaseUrl, + }) + } + return { valid: false, status: 'error', diff --git a/src/main/providers/catalog.generated.ts b/src/main/providers/catalog.generated.ts index f05aedd..eaf6165 100644 --- a/src/main/providers/catalog.generated.ts +++ b/src/main/providers/catalog.generated.ts @@ -5,7 +5,7 @@ * src/main/providers/catalog.source.json * and run `npm run gen:catalog`. * - * fingerprint: 3d35ef2c7f6a58e0 (entries: 18) + * fingerprint: e96a0df0e32f36db (entries: 18) */ import type { ProviderEntry } from './types' @@ -68,7 +68,7 @@ export const PROVIDER_CATALOG: readonly ProviderEntry[] = "envKeys": [], "authType": "cloud_creds", "probeKind": "gcp", - "notes": "Uses ADC. Configure project + region in your gateway." + "notes": "Uses ADC. Wizard will ask for project + region first." }, { "id": "bedrock", @@ -79,7 +79,7 @@ export const PROVIDER_CATALOG: readonly ProviderEntry[] = "envKeys": [], "authType": "cloud_creds", "probeKind": "aws", - "notes": "Uses AWS credentials from the standard chain." + "notes": "Uses AWS credentials from the standard chain. Wizard validates them with a Bedrock ListFoundationModels call." }, { "id": "azure-openai", diff --git a/src/main/providers/catalog.source.json b/src/main/providers/catalog.source.json index 6b861d6..e565325 100644 --- a/src/main/providers/catalog.source.json +++ b/src/main/providers/catalog.source.json @@ -41,7 +41,7 @@ "envKeys": [], "authType": "cloud_creds", "probeKind": "gcp", - "notes": "Uses ADC. Configure project + region in your gateway." + "notes": "Uses ADC. Wizard will ask for project + region first." }, { "id": "bedrock", @@ -50,7 +50,7 @@ "envKeys": [], "authType": "cloud_creds", "probeKind": "aws", - "notes": "Uses AWS credentials from the standard chain." + "notes": "Uses AWS credentials from the standard chain. Wizard validates them with a Bedrock ListFoundationModels call." }, { "id": "azure-openai",