From e666c80012a532d1e804e1167649dca19f829240 Mon Sep 17 00:00:00 2001 From: prajapatiy9826 Date: Wed, 17 Jun 2026 13:02:11 +0530 Subject: [PATCH 1/2] nodejs/autonomous/github-trending: support Foundry endpoints + accept canonical CLI env-var names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three improvements to the Node.js github-trending sample: 1. Foundry / AI Services endpoint support - Detect *.services.ai.azure.com / *.cognitiveservices.azure.com hosts and use a plain OpenAI client at /openai/v1 with an api-key default header (the /openai/v1 path rejects the api-version query parameter that AzureOpenAI would otherwise append). - Strip any path the user pasted from the Foundry portal (e.g. /api/projects/, /openai/v1, /openai/v1/responses) so the endpoint normalizes to scheme + host before client construction. - Classic *.openai.azure.com endpoints continue to use AzureOpenAI with apiVersion (existing behavior). - Make AZURE_OPENAI_API_VERSION configurable via env (default 2024-10-21); was previously hard-coded to 2024-12-01-preview. 2. Accept canonical agent365Observability__* env-var names - The Agent 365 CLI (a365 setup all) auto-stamps these into .env. - Read them first, fall back to legacy AGENT365_* names for backward compatibility with hand-edited or older .env files. 3. Optional observability feature flags (parity with Python sample) - ENABLE_A365_OBSERVABILITY (default true) — master switch; false skips useMicrosoftOpenTelemetry() entirely. - ENABLE_A365_OBSERVABILITY_EXPORTER (default false) — when false, spans are produced but go only to the console exporter; set to true to upload to the A365 observability backend. .env.template and README.md are updated to document Foundry support, both canonical and legacy env-var names side-by-side, and the observability flags. Validation: - End-to-end run against a Foundry account with a fresh agent identity: - chat.completions returned HTTP 200 (Foundry path /openai/v1). - 3-hop FMI token chain registered initial + refresh tokens. - Span tree invoke_agent -> Chat -> execute_tool with matching traceId. - A365 backend export succeeded (correlation IDs captured). - HTTP /api/health returns 200, / returns 200, /nonexistent returns 404. - Heartbeat fires every 60 s; SIGINT triggers graceful shutdown. - Legacy AGENT365_* env-var fallback verified by removing canonical names. - No secrets, GUIDs, or API keys land in the diff (.env and a365.*config.json are in root .gitignore). --- .../autonomous/github-trending/.env.template | 31 ++++++++++--- nodejs/autonomous/github-trending/README.md | 41 ++++++++++++----- .../src/github-trending-service.ts | 36 +++++++++++---- .../autonomous/github-trending/src/index.ts | 44 ++++++++++++++----- 4 files changed, 116 insertions(+), 36 deletions(-) diff --git a/nodejs/autonomous/github-trending/.env.template b/nodejs/autonomous/github-trending/.env.template index c3de0818..76bdfba6 100644 --- a/nodejs/autonomous/github-trending/.env.template +++ b/nodejs/autonomous/github-trending/.env.template @@ -1,20 +1,36 @@ # Azure OpenAI +# Both classic Azure OpenAI (.openai.azure.com) and Foundry endpoints +# (services.ai.azure.com / cognitiveservices.azure.com) are supported. +# Any path you paste (e.g. /api/projects/.../openai/v1/responses) is stripped +# automatically by github-trending-service.ts. AZURE_OPENAI_ENDPOINT=<> AZURE_OPENAI_API_KEY=<> AZURE_OPENAI_DEPLOYMENT=gpt-4o +# Pick an API version supported by your Azure OpenAI / Foundry resource (e.g. 2024-10-21 GA, 2024-08-01-preview, 2025-01-01-preview). +# Foundry endpoints use /openai/v1 and ignore this value. +AZURE_OPENAI_API_VERSION=2024-10-21 # Agent 365 Observability -AGENT365_TENANT_ID=<> -AGENT365_AGENT_ID=<> -AGENT365_BLUEPRINT_ID=<> -AGENT365_CLIENT_ID=<> -AGENT365_CLIENT_SECRET=<> -AGENT365_AGENT_NAME=<> -AGENT365_AGENT_DESCRIPTION=<> +# Canonical (preferred) keys: agent365Observability__* convention used by Microsoft Agents SDK. +# Legacy AGENT365_* names are still accepted as a fallback by src/index.ts. +agent365Observability__tenantId=<> +agent365Observability__agentId=<> +agent365Observability__agentBlueprintId=<> +agent365Observability__clientId=<> +agent365Observability__clientSecret=<> +agent365Observability__agentName=<> +agent365Observability__agentDescription=<> # Auth strategy: set to "true" for production (MSI), "false" for local dev (client secret) AGENT365_USE_MANAGED_IDENTITY=false +# Observability feature flags +# ENABLE_A365_OBSERVABILITY — master switch for the OpenTelemetry pipeline. +# ENABLE_A365_OBSERVABILITY_EXPORTER — when false, spans go to console only +# (no upload to the A365 backend). +ENABLE_A365_OBSERVABILITY=true +ENABLE_A365_OBSERVABILITY_EXPORTER=true + # GitHub Trending GITHUB_TRENDING_LANGUAGE=typescript GITHUB_TRENDING_MIN_STARS=5 @@ -24,3 +40,4 @@ GITHUB_TRENDING_MAX_RESULTS=10 HEARTBEAT_INTERVAL_MS=60000 PORT=3979 NODE_ENV=development + diff --git a/nodejs/autonomous/github-trending/README.md b/nodejs/autonomous/github-trending/README.md index ca4989d2..0389fea3 100644 --- a/nodejs/autonomous/github-trending/README.md +++ b/nodejs/autonomous/github-trending/README.md @@ -55,25 +55,42 @@ a365 setup permissions custom --agent-name --resource-app-id 9 ### Configuration -Copy `.env.template` to `.env` and fill in the values from `a365 setup all` output: +Copy `.env.template` to `.env`. Most values are auto-stamped by `a365 setup all`; fill in the Azure OpenAI fields manually. ```bash cp .env.template .env ``` +#### Azure OpenAI + | Variable | Set by | Description | |----------|--------|-------------| -| `AZURE_OPENAI_ENDPOINT` | Manual | Azure OpenAI resource endpoint | +| `AZURE_OPENAI_ENDPOINT` | Manual | Azure OpenAI resource endpoint. Both classic Azure OpenAI (`*.openai.azure.com`) and Foundry / AI Services (`*.services.ai.azure.com`, `*.cognitiveservices.azure.com`) are supported. Any path you paste from the Foundry portal (e.g. `/api/projects/.../openai/v1/responses`) is stripped automatically. | | `AZURE_OPENAI_API_KEY` | Manual | Azure OpenAI API key | | `AZURE_OPENAI_DEPLOYMENT` | Manual | Model deployment name (default: `gpt-4o`) | -| `AGENT365_TENANT_ID` | CLI | Entra tenant ID | -| `AGENT365_AGENT_ID` | CLI | Agent identity ID (separate from blueprint) | -| `AGENT365_BLUEPRINT_ID` | CLI | Blueprint app registration ID | -| `AGENT365_CLIENT_ID` | CLI | Blueprint app ID (same as blueprint ID) | -| `AGENT365_CLIENT_SECRET` | CLI | Blueprint client secret | -| `AGENT365_AGENT_NAME` | CLI | Display name shown in traces | -| `AGENT365_AGENT_DESCRIPTION` | CLI | Agent description shown in traces | -| `AGENT365_USE_MANAGED_IDENTITY` | Manual | `true` for production (MSI), `false` for local dev. **Defaults to `true` when unset.** | +| `AZURE_OPENAI_API_VERSION` | Manual | API version for **classic** Azure OpenAI endpoints only. Foundry endpoints use `/openai/v1` and ignore this value. Default: `2024-10-21`. | + +#### Agent 365 observability credentials + +The Agent 365 CLI stamps these values directly into `.env` when you run `a365 setup all`. Both the canonical `agent365Observability__*` keys and legacy `AGENT365_*` aliases are accepted; the canonical keys are preferred and take precedence when both are present. + +| Variable (canonical) | Legacy alias | Set by | Description | +|----------------------|--------------|--------|-------------| +| `agent365Observability__tenantId` | `AGENT365_TENANT_ID` | CLI | Entra tenant ID | +| `agent365Observability__agentId` | `AGENT365_AGENT_ID` | CLI | Agent identity ID (separate from blueprint) | +| `agent365Observability__agentBlueprintId` | `AGENT365_BLUEPRINT_ID` | CLI | Blueprint app registration ID | +| `agent365Observability__clientId` | `AGENT365_CLIENT_ID` | CLI | Blueprint app ID (same as blueprint ID) | +| `agent365Observability__clientSecret` | `AGENT365_CLIENT_SECRET` | CLI | Blueprint client secret | +| `agent365Observability__agentName` | `AGENT365_AGENT_NAME` | CLI | Display name shown in traces | +| `agent365Observability__agentDescription` | `AGENT365_AGENT_DESCRIPTION` | CLI | Agent description shown in traces | +| `AGENT365_USE_MANAGED_IDENTITY` | — | Manual | `true` for production (MSI), `false` for local dev. **Defaults to `true` when unset.** | + +#### Observability feature flags + +| Variable | Default | Description | +|----------|---------|-------------| +| `ENABLE_A365_OBSERVABILITY` | `true` | Master switch for the OpenTelemetry pipeline. Set `false` to skip `useMicrosoftOpenTelemetry()` entirely (no spans generated). | +| `ENABLE_A365_OBSERVABILITY_EXPORTER` | `false` | When `true`, spans are uploaded to the A365 observability backend. When `false`, spans are produced but only printed to the console exporter. **Set to `true` to ship traces to the A365 portal.** | ### GitHub Trending Configuration @@ -93,7 +110,11 @@ The GitHub Search API is unauthenticated — no API key required (rate limit: 10 You can run the agent with **just Azure OpenAI credentials** — no Agent 365 setup required. Create a minimal `.env`: ```bash +# Classic Azure OpenAI: AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ +# — or Foundry / AI Services: +# AZURE_OPENAI_ENDPOINT=https://your-foundry-account.services.ai.azure.com/ + AZURE_OPENAI_API_KEY=your-api-key AZURE_OPENAI_DEPLOYMENT=gpt-4o AGENT365_USE_MANAGED_IDENTITY=false diff --git a/nodejs/autonomous/github-trending/src/github-trending-service.ts b/nodejs/autonomous/github-trending/src/github-trending-service.ts index 85fac702..5da0ed61 100644 --- a/nodejs/autonomous/github-trending/src/github-trending-service.ts +++ b/nodejs/autonomous/github-trending/src/github-trending-service.ts @@ -7,7 +7,7 @@ * and how to call the GitHub Search API. */ -import { AzureOpenAI } from 'openai'; +import OpenAI, { AzureOpenAI } from 'openai'; import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources/chat/completions'; import { @@ -34,6 +34,7 @@ export interface TrendingServiceConfig { endpoint: string; apiKey: string; deployment: string; + apiVersion: string; agentDetails: AgentDetails; language: string; minStars: number; @@ -49,12 +50,31 @@ export interface TrendingServiceConfig { * @param delayFirstRunMs Optional delay (ms) before the first cycle, e.g. to let token service warm up. */ export function startTrendingService(config: TrendingServiceConfig, delayFirstRunMs: number = 0): AbortController { - const client = new AzureOpenAI({ - endpoint: config.endpoint, - apiKey: config.apiKey, - apiVersion: '2024-12-01-preview', - deployment: config.deployment, - }); + // Foundry resources (services.ai.azure.com / cognitiveservices.azure.com) use the + // OpenAI-compatible /openai/v1 path which does NOT accept the api-version query. + // Classic Azure OpenAI resources (.openai.azure.com) use the legacy deployments path. + const useFoundryV1Path = + /services\.ai\.azure\.com|cognitiveservices\.azure\.com/i.test(config.endpoint) || + /^preview$/i.test(config.apiVersion); + + // Normalize the endpoint to just :// — strip any path the user may have + // pasted from the Foundry portal (e.g. /api/projects/, /openai/v1, /openai/v1/responses). + const parsedEndpoint = new URL(config.endpoint); + const resourceEndpoint = `${parsedEndpoint.protocol}//${parsedEndpoint.host}`; + + const foundryBaseURL = `${resourceEndpoint}/openai/v1`; + const client: OpenAI = useFoundryV1Path + ? new OpenAI({ + baseURL: foundryBaseURL, + apiKey: config.apiKey, + defaultHeaders: { 'api-key': config.apiKey }, + }) + : new AzureOpenAI({ + endpoint: resourceEndpoint, + apiKey: config.apiKey, + apiVersion: config.apiVersion, + deployment: config.deployment, + }); console.log(`GitHubTrendingService started. Interval: ${config.intervalMs}ms`); @@ -81,7 +101,7 @@ export function startTrendingService(config: TrendingServiceConfig, delayFirstRu return controller; } -async function runCycle(client: AzureOpenAI, config: TrendingServiceConfig): Promise { +async function runCycle(client: OpenAI, config: TrendingServiceConfig): Promise { const { deployment, agentDetails, endpoint, language, minStars, maxResults } = config; // A365 Observability — propagate baggage context for this cycle. diff --git a/nodejs/autonomous/github-trending/src/index.ts b/nodejs/autonomous/github-trending/src/index.ts index a95cfcfe..e468ea1d 100644 --- a/nodejs/autonomous/github-trending/src/index.ts +++ b/nodejs/autonomous/github-trending/src/index.ts @@ -33,18 +33,28 @@ const shutdownHandles: { intervals: ReturnType[]; controller const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT!; const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY!; const AZURE_OPENAI_DEPLOYMENT = process.env.AZURE_OPENAI_DEPLOYMENT || 'gpt-4o'; +const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION || '2024-10-21'; // Agent 365 Observability — optional. When these are missing or set to placeholders, // the agent runs without A365 observability export (spans go to console only). -const TENANT_ID = process.env.AGENT365_TENANT_ID || ''; -const AGENT_ID = process.env.AGENT365_AGENT_ID || ''; -const BLUEPRINT_ID = process.env.AGENT365_BLUEPRINT_ID || ''; -const CLIENT_ID = process.env.AGENT365_CLIENT_ID || ''; -const CLIENT_SECRET = process.env.AGENT365_CLIENT_SECRET || ''; -const AGENT_NAME = process.env.AGENT365_AGENT_NAME || 'github-trending'; -const AGENT_DESCRIPTION = process.env.AGENT365_AGENT_DESCRIPTION || ''; +// Read agent365Observability__* (canonical) with legacy AGENT365_* names as fallback +// for backward compatibility with older .env files. +const TENANT_ID = process.env.agent365Observability__tenantId || process.env.AGENT365_TENANT_ID || ''; +const AGENT_ID = process.env.agent365Observability__agentId || process.env.AGENT365_AGENT_ID || ''; +const BLUEPRINT_ID = process.env.agent365Observability__agentBlueprintId || process.env.AGENT365_BLUEPRINT_ID || ''; +const CLIENT_ID = process.env.agent365Observability__clientId || process.env.AGENT365_CLIENT_ID || ''; +const CLIENT_SECRET = process.env.agent365Observability__clientSecret || process.env.AGENT365_CLIENT_SECRET || ''; +const AGENT_NAME = process.env.agent365Observability__agentName || process.env.AGENT365_AGENT_NAME || 'github-trending'; +const AGENT_DESCRIPTION = process.env.agent365Observability__agentDescription || process.env.AGENT365_AGENT_DESCRIPTION || ''; const USE_MANAGED_IDENTITY = (process.env.AGENT365_USE_MANAGED_IDENTITY || 'true').toLowerCase() === 'true'; +// Observability feature flags (mirrors python/autonomous/github-trending/main.py). +// ENABLE_A365_OBSERVABILITY — master switch for the OpenTelemetry pipeline. +// ENABLE_A365_OBSERVABILITY_EXPORTER — when false, spans go to console only +// (no upload to the A365 backend). +const ENABLE_A365_OBSERVABILITY = (process.env.ENABLE_A365_OBSERVABILITY || 'true').toLowerCase() === 'true'; +const ENABLE_A365_OBSERVABILITY_EXPORTER = (process.env.ENABLE_A365_OBSERVABILITY_EXPORTER || 'false').toLowerCase() === 'true'; + function hasA365Credentials(): boolean { const requiredValues = [TENANT_ID, AGENT_ID, CLIENT_ID]; const hasRequiredValues = requiredValues.every(v => v && !v.startsWith('<<')); @@ -61,6 +71,9 @@ function hasA365Credentials(): boolean { } const A365_ENABLED = hasA365Credentials(); +// Exporter is only active when (a) the master observability flag is on, +// (b) credentials are configured, and (c) the exporter flag is on. +const A365_EXPORTER_ENABLED = ENABLE_A365_OBSERVABILITY && A365_ENABLED && ENABLE_A365_OBSERVABILITY_EXPORTER; const LANGUAGE = process.env.GITHUB_TRENDING_LANGUAGE || 'typescript'; const MIN_STARS = parseInt(process.env.GITHUB_TRENDING_MIN_STARS || '5', 10); @@ -87,7 +100,8 @@ const agentDetails: AgentDetails = { // Build A365 span processors manually so we can set useS2SEndpoint (autonomous S2S scenario). // The distro's a365 option doesn't yet expose useS2SEndpoint, so we create the exporter ourselves // and pass it via spanProcessors, leaving a365 unset to avoid a duplicate exporter. -const a365SpanProcessors = A365_ENABLED +// Exporter is only attached when both credentials are present AND the exporter flag is on. +const a365SpanProcessors = A365_EXPORTER_ENABLED ? [ new A365SpanProcessor(), new BatchSpanProcessor(new Agent365Exporter({ @@ -98,9 +112,16 @@ const a365SpanProcessors = A365_ENABLED ] : []; -useMicrosoftOpenTelemetry({ - spanProcessors: a365SpanProcessors, -}); +if (ENABLE_A365_OBSERVABILITY) { + useMicrosoftOpenTelemetry({ + spanProcessors: a365SpanProcessors, + }); + console.log( + `Observability configured (a365_exporter=${A365_EXPORTER_ENABLED}, credentials_present=${A365_ENABLED})` + ); +} else { + console.log('Observability disabled (ENABLE_A365_OBSERVABILITY=false)'); +} // ── Express server ─────────────────────────────────────────────────────────── @@ -149,6 +170,7 @@ server.listen(PORT, host, () => { endpoint: AZURE_OPENAI_ENDPOINT, apiKey: AZURE_OPENAI_API_KEY, deployment: AZURE_OPENAI_DEPLOYMENT, + apiVersion: AZURE_OPENAI_API_VERSION, agentDetails, language: LANGUAGE, minStars: MIN_STARS, From 659a62e9be6b027b584f7c485c2345dcba9cfb5d Mon Sep 17 00:00:00 2001 From: prajapatiy9826 Date: Wed, 17 Jun 2026 14:43:00 +0530 Subject: [PATCH 2/2] nodejs/autonomous/github-trending: address Copilot review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the 4 review comments on PR #323: 1. URL parsing safety — wrap `new URL(config.endpoint)` in try/catch and validate that the parsed URL has an http(s) scheme and a host. Misconfigured values now produce a clear "must be an absolute URL" error instead of a confusing TypeError at startup. 2. Drop the `^preview$` apiVersion sentinel — it never matched the documented examples (`2024-08-01-preview`, `2025-01-01-preview`). Foundry detection now relies entirely on the endpoint hostname, which is the documented and predictable signal. 3. Make `apiVersion` optional in `TrendingServiceConfig` — defaults to `2024-10-21` inside `startTrendingService()` so the exported interface is no longer a breaking change for external callers, while preserving the environment-driven override path through `src/index.ts`. 4. Document why the Foundry path passes the API key via both `apiKey` and `defaultHeaders["api-key"]` — the SDK requires `apiKey` on construction; Foundry uses the `api-key` header for auth and ignores the resulting `Authorization: Bearer` header. 5. Flip `.env.template` `ENABLE_A365_OBSERVABILITY_EXPORTER` default to `false` to match the runtime default and the README. Added a comment pointing users at `true` as the explicit opt-in once credentials are configured, so copying the template no longer silently uploads traces. --- .../autonomous/github-trending/.env.template | 4 +- .../src/github-trending-service.ts | 43 ++++++++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/nodejs/autonomous/github-trending/.env.template b/nodejs/autonomous/github-trending/.env.template index 76bdfba6..010a6325 100644 --- a/nodejs/autonomous/github-trending/.env.template +++ b/nodejs/autonomous/github-trending/.env.template @@ -28,8 +28,10 @@ AGENT365_USE_MANAGED_IDENTITY=false # ENABLE_A365_OBSERVABILITY — master switch for the OpenTelemetry pipeline. # ENABLE_A365_OBSERVABILITY_EXPORTER — when false, spans go to console only # (no upload to the A365 backend). +# Set ENABLE_A365_OBSERVABILITY_EXPORTER=true to ship traces to the A365 portal +# once your Agent 365 credentials above are configured. ENABLE_A365_OBSERVABILITY=true -ENABLE_A365_OBSERVABILITY_EXPORTER=true +ENABLE_A365_OBSERVABILITY_EXPORTER=false # GitHub Trending GITHUB_TRENDING_LANGUAGE=typescript diff --git a/nodejs/autonomous/github-trending/src/github-trending-service.ts b/nodejs/autonomous/github-trending/src/github-trending-service.ts index 5da0ed61..d9ee1cc8 100644 --- a/nodejs/autonomous/github-trending/src/github-trending-service.ts +++ b/nodejs/autonomous/github-trending/src/github-trending-service.ts @@ -34,7 +34,12 @@ export interface TrendingServiceConfig { endpoint: string; apiKey: string; deployment: string; - apiVersion: string; + /** + * API version for the classic AzureOpenAI client. Ignored when the endpoint host + * matches a Foundry / AI Services pattern (the `/openai/v1` path rejects `api-version`). + * Defaults to `2024-10-21` when omitted. + */ + apiVersion?: string; agentDetails: AgentDetails; language: string; minStars: number; @@ -50,29 +55,47 @@ export interface TrendingServiceConfig { * @param delayFirstRunMs Optional delay (ms) before the first cycle, e.g. to let token service warm up. */ export function startTrendingService(config: TrendingServiceConfig, delayFirstRunMs: number = 0): AbortController { - // Foundry resources (services.ai.azure.com / cognitiveservices.azure.com) use the - // OpenAI-compatible /openai/v1 path which does NOT accept the api-version query. - // Classic Azure OpenAI resources (.openai.azure.com) use the legacy deployments path. - const useFoundryV1Path = - /services\.ai\.azure\.com|cognitiveservices\.azure\.com/i.test(config.endpoint) || - /^preview$/i.test(config.apiVersion); + const apiVersion = config.apiVersion ?? '2024-10-21'; // Normalize the endpoint to just :// — strip any path the user may have // pasted from the Foundry portal (e.g. /api/projects/, /openai/v1, /openai/v1/responses). - const parsedEndpoint = new URL(config.endpoint); + // Validate up-front so misconfigured values fail with a clear message rather than a + // confusing TypeError from the URL constructor. + let parsedEndpoint: URL; + try { + parsedEndpoint = new URL(config.endpoint); + } catch { + throw new Error( + `AZURE_OPENAI_ENDPOINT must be an absolute URL (e.g. https://.openai.azure.com/), got: ${config.endpoint}`, + ); + } + if (!/^https?:$/.test(parsedEndpoint.protocol) || !parsedEndpoint.host) { + throw new Error( + `AZURE_OPENAI_ENDPOINT must be an absolute http(s) URL with a host, got: ${config.endpoint}`, + ); + } const resourceEndpoint = `${parsedEndpoint.protocol}//${parsedEndpoint.host}`; + // Foundry resources (services.ai.azure.com / cognitiveservices.azure.com) use the + // OpenAI-compatible /openai/v1 path which does NOT accept the api-version query. + // Classic Azure OpenAI resources (.openai.azure.com) use the legacy deployments path. + const useFoundryV1Path = /services\.ai\.azure\.com|cognitiveservices\.azure\.com/i.test(parsedEndpoint.host); + const foundryBaseURL = `${resourceEndpoint}/openai/v1`; + // Foundry's /openai/v1 path authenticates via the `api-key` request header. The OpenAI SDK + // requires `apiKey` on construction, so we pass a placeholder string — the real credential + // is sent via `defaultHeaders["api-key"]` and Foundry ignores the placeholder Bearer token. + // (Mirrors the placeholder-auth pattern in python/autonomous/github-trending/main.py.) const client: OpenAI = useFoundryV1Path ? new OpenAI({ baseURL: foundryBaseURL, - apiKey: config.apiKey, + apiKey: 'placeholder-foundry-uses-api-key-header', defaultHeaders: { 'api-key': config.apiKey }, }) : new AzureOpenAI({ endpoint: resourceEndpoint, apiKey: config.apiKey, - apiVersion: config.apiVersion, + apiVersion, deployment: config.deployment, });