Skip to content

Commit 56f6345

Browse files
tylergibbs1claude
andcommitted
Add Entra ID auth, misc improvements, bump to v0.7.0
- Add azureAdTokenProvider to both Azure model configs for Microsoft Entra ID (bearer token) auth alongside existing API key auth - Constructor validates exactly one auth method provided - Token provider called fresh on each request (no caching) - 10 new auth unit tests - Minor fixes to agent, builtin-tools, hosted-tool, model, run, types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5f80a32 commit 56f6345

12 files changed

Lines changed: 1095 additions & 33 deletions

File tree

examples/test-battle.ts

Lines changed: 768 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stratus-sdk",
3-
"version": "0.6.3",
3+
"version": "0.7.0",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/azure/chat-completions-model.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import type {
88
StreamEvent,
99
UsageInfo,
1010
} from "../core/model";
11-
import type { ChatMessage, ToolCall, ToolDefinition } from "../core/types";
11+
import type { ChatMessage, HostedToolDefinition, ToolCall, ToolDefinition } from "../core/types";
1212
import { resolveChatCompletionsUrl } from "./endpoint";
1313
import { parseSSE } from "./sse-parser";
1414

1515
export interface AzureChatCompletionsModelConfig {
1616
endpoint: string;
17-
apiKey: string;
17+
apiKey?: string;
18+
azureAdTokenProvider?: () => Promise<string>;
1819
deployment: string;
1920
apiVersion?: string;
2021
}
@@ -23,11 +24,23 @@ const DEFAULT_API_VERSION = "2025-03-01-preview";
2324

2425
export class AzureChatCompletionsModel implements Model {
2526
private readonly url: string;
26-
private readonly apiKey: string;
27+
private readonly apiKey?: string;
28+
private readonly tokenProvider?: () => Promise<string>;
2729
private readonly deployment: string;
2830

2931
constructor(config: AzureChatCompletionsModelConfig) {
32+
if (config.apiKey && config.azureAdTokenProvider) {
33+
throw new StratusError(
34+
"Provide either apiKey or azureAdTokenProvider, not both",
35+
);
36+
}
37+
if (!config.apiKey && !config.azureAdTokenProvider) {
38+
throw new StratusError(
39+
"Provide either apiKey or azureAdTokenProvider",
40+
);
41+
}
3042
this.apiKey = config.apiKey;
43+
this.tokenProvider = config.azureAdTokenProvider;
3144
this.deployment = config.deployment;
3245
this.url = resolveChatCompletionsUrl(
3346
config.endpoint,
@@ -36,6 +49,14 @@ export class AzureChatCompletionsModel implements Model {
3649
);
3750
}
3851

52+
private async getAuthHeaders(): Promise<Record<string, string>> {
53+
if (this.tokenProvider) {
54+
const token = await this.tokenProvider();
55+
return { Authorization: `Bearer ${token}` };
56+
}
57+
return { "api-key": this.apiKey! };
58+
}
59+
3960
async getResponse(
4061
request: ModelRequest,
4162
options?: ModelRequestOptions,
@@ -161,14 +182,8 @@ export class AzureChatCompletionsModel implements Model {
161182
}
162183

163184
if (request.tools && request.tools.length > 0) {
164-
for (const tool of request.tools) {
165-
if (!("function" in tool)) {
166-
throw new StratusError(
167-
"Hosted tools (web_search, code_interpreter, mcp, image_generation) are not supported by the Chat Completions API. Use AzureResponsesModel instead.",
168-
);
169-
}
170-
}
171-
body.tools = request.tools as ToolDefinition[];
185+
assertAllFunctionTools(request.tools);
186+
body.tools = request.tools;
172187
}
173188

174189
if (request.responseFormat) {
@@ -201,11 +216,12 @@ export class AzureChatCompletionsModel implements Model {
201216
): Promise<Response> {
202217
const maxRetries = 3;
203218
for (let attempt = 0; attempt <= maxRetries; attempt++) {
219+
const authHeaders = await this.getAuthHeaders();
204220
const response = await fetch(this.url, {
205221
method: "POST",
206222
headers: {
207223
"Content-Type": "application/json",
208-
"api-key": this.apiKey,
224+
...authHeaders,
209225
},
210226
body: JSON.stringify(body),
211227
signal,
@@ -300,6 +316,18 @@ export class AzureChatCompletionsModel implements Model {
300316
}
301317
}
302318

319+
function assertAllFunctionTools(
320+
tools: (ToolDefinition | HostedToolDefinition)[],
321+
): asserts tools is ToolDefinition[] {
322+
for (const tool of tools) {
323+
if (!("function" in tool)) {
324+
throw new StratusError(
325+
"Hosted tools (web_search, code_interpreter, mcp, image_generation) are not supported by the Chat Completions API. Use AzureResponsesModel instead.",
326+
);
327+
}
328+
}
329+
}
330+
303331
function serializeMessage(msg: ChatMessage): Record<string, unknown> {
304332
switch (msg.role) {
305333
case "system":

src/azure/responses-model.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ContentFilterError, ModelError } from "../core/errors";
1+
import { ContentFilterError, ModelError, StratusError } from "../core/errors";
22
import type {
33
FinishReason,
44
Model,
@@ -11,6 +11,7 @@ import type {
1111
import type {
1212
ChatMessage,
1313
ContentPart,
14+
HostedToolDefinition,
1415
ResponseFormat,
1516
ToolCall,
1617
ToolChoice,
@@ -21,7 +22,8 @@ import { parseSSE } from "./sse-parser";
2122

2223
export interface AzureResponsesModelConfig {
2324
endpoint: string;
24-
apiKey: string;
25+
apiKey?: string;
26+
azureAdTokenProvider?: () => Promise<string>;
2527
deployment: string;
2628
apiVersion?: string;
2729
store?: boolean;
@@ -31,12 +33,24 @@ const DEFAULT_API_VERSION = "2025-04-01-preview";
3133

3234
export class AzureResponsesModel implements Model {
3335
private readonly url: string;
34-
private readonly apiKey: string;
36+
private readonly apiKey?: string;
37+
private readonly tokenProvider?: () => Promise<string>;
3538
private readonly deployment: string;
3639
private readonly store: boolean;
3740

3841
constructor(config: AzureResponsesModelConfig) {
42+
if (config.apiKey && config.azureAdTokenProvider) {
43+
throw new StratusError(
44+
"Provide either apiKey or azureAdTokenProvider, not both",
45+
);
46+
}
47+
if (!config.apiKey && !config.azureAdTokenProvider) {
48+
throw new StratusError(
49+
"Provide either apiKey or azureAdTokenProvider",
50+
);
51+
}
3952
this.apiKey = config.apiKey;
53+
this.tokenProvider = config.azureAdTokenProvider;
4054
this.deployment = config.deployment;
4155
this.store = config.store ?? false;
4256
this.url = resolveResponsesUrl(
@@ -45,6 +59,14 @@ export class AzureResponsesModel implements Model {
4559
);
4660
}
4761

62+
private async getAuthHeaders(): Promise<Record<string, string>> {
63+
if (this.tokenProvider) {
64+
const token = await this.tokenProvider();
65+
return { Authorization: `Bearer ${token}` };
66+
}
67+
return { "api-key": this.apiKey! };
68+
}
69+
4870
async getResponse(
4971
request: ModelRequest,
5072
options?: ModelRequestOptions,
@@ -233,11 +255,12 @@ export class AzureResponsesModel implements Model {
233255
): Promise<Response> {
234256
const maxRetries = 3;
235257
for (let attempt = 0; attempt <= maxRetries; attempt++) {
258+
const authHeaders = await this.getAuthHeaders();
236259
const response = await fetch(this.url, {
237260
method: "POST",
238261
headers: {
239262
"Content-Type": "application/json",
240-
"api-key": this.apiKey,
263+
...authHeaders,
241264
},
242265
body: JSON.stringify(body),
243266
signal,
@@ -440,14 +463,18 @@ function convertUserContent(
440463
}
441464

442465
function isFunctionToolDefinition(
443-
def: ToolDefinition | Record<string, unknown>,
466+
def: ToolDefinition | HostedToolDefinition,
444467
): def is ToolDefinition {
445-
return "function" in def && typeof (def as ToolDefinition).function === "object";
468+
return "function" in def && (def as ToolDefinition).function != null && typeof (def as ToolDefinition).function === "object";
446469
}
447470

448-
function convertToolChoice(
449-
toolChoice: ToolChoice,
450-
): string | { type: string; name: string } {
471+
type ResponsesToolChoice =
472+
| "auto"
473+
| "none"
474+
| "required"
475+
| { type: "function"; name: string };
476+
477+
function convertToolChoice(toolChoice: ToolChoice): ResponsesToolChoice {
451478
if (typeof toolChoice === "string") return toolChoice;
452479
// Chat Completions format: { type: "function", function: { name } }
453480
// Responses API format: { type: "function", name }

src/core/agent.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { AgentHooks } from "./hooks";
55
import type { Model } from "./model";
66
import type { SubAgent } from "./subagent";
77
import type { AgentTool } from "./hosted-tool";
8-
import type { FunctionTool } from "./tool";
98
import type { ModelSettings, ResponseFormat, ToolUseBehavior } from "./types";
109
import { zodToJsonSchema } from "./utils/zod";
1110

src/core/builtin-tools.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { HostedTool } from "./hosted-tool";
2+
import type { HostedToolDefinition } from "./types";
23

34
export interface WebSearchToolConfig {
45
userLocation?: {
@@ -12,7 +13,7 @@ export interface WebSearchToolConfig {
1213
}
1314

1415
export function webSearchTool(config?: WebSearchToolConfig): HostedTool {
15-
const definition: Record<string, unknown> = {
16+
const definition: HostedToolDefinition = {
1617
type: "web_search_preview",
1718
};
1819
if (config?.userLocation) {
@@ -53,7 +54,7 @@ export interface McpToolConfig {
5354
}
5455

5556
export function mcpTool(config: McpToolConfig): HostedTool {
56-
const definition: Record<string, unknown> = {
57+
const definition: HostedToolDefinition = {
5758
type: "mcp",
5859
server_label: config.serverLabel,
5960
server_url: config.serverUrl,

src/core/hosted-tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import type { HostedToolDefinition } from "./types";
12
import type { FunctionTool } from "./tool";
23

34
export interface HostedTool {
45
type: "hosted";
56
name: string;
6-
definition: Record<string, unknown>;
7+
definition: HostedToolDefinition;
78
}
89

910
export type AgentTool = FunctionTool | HostedTool;

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export type {
6767
ToolMessage,
6868
ToolCall,
6969
ToolDefinition,
70+
HostedToolDefinition,
7071
ModelSettings,
7172
ReasoningEffort,
7273
ResponseFormat,

src/core/model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import type { ChatMessage, ModelSettings, ResponseFormat, ToolCall, ToolDefinition } from "./types";
1+
import type { ChatMessage, HostedToolDefinition, ModelSettings, ResponseFormat, ToolCall, ToolDefinition } from "./types";
22

33
export interface ModelRequest {
44
messages: ChatMessage[];
5-
tools?: (ToolDefinition | Record<string, unknown>)[];
5+
tools?: (ToolDefinition | HostedToolDefinition)[];
66
modelSettings?: ModelSettings;
77
responseFormat?: ResponseFormat;
88
previousResponseId?: string;

src/core/run.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import { subagentToDefinition, subagentToTool } from "./subagent";
2525
import type { SubAgent } from "./subagent";
2626
import { RunResult } from "./result";
2727
import { toolToDefinition } from "./tool";
28-
import type { FunctionTool } from "./tool";
28+
2929
import { getCurrentTrace } from "./tracing";
30-
import type { AssistantMessage, ChatMessage, ToolCall, ToolDefinition, ToolMessage } from "./types";
30+
import type { AssistantMessage, ChatMessage, HostedToolDefinition, ToolCall, ToolDefinition, ToolMessage } from "./types";
3131

3232
const DEFAULT_MAX_TURNS = 10;
3333

@@ -675,8 +675,8 @@ async function buildFinalResult<TContext, TOutput>(
675675
return result;
676676
}
677677

678-
function buildToolDefs(agent: Agent<any, any>): (ToolDefinition | Record<string, unknown>)[] {
679-
const defs: (ToolDefinition | Record<string, unknown>)[] = [];
678+
function buildToolDefs(agent: Agent<any, any>): (ToolDefinition | HostedToolDefinition)[] {
679+
const defs: (ToolDefinition | HostedToolDefinition)[] = [];
680680
for (const t of agent.tools) {
681681
if (isHostedTool(t)) {
682682
defs.push(t.definition);
@@ -737,7 +737,7 @@ async function executeToolCallsWithHandoffs<TContext>(
737737
// Build O(1) lookup maps
738738
const handoffsByName = new Map(agent.handoffs.map((h) => [h.toolName, h]));
739739
const subagentsByName = new Map(agent.subagents.map((sa) => [sa.toolName, sa]));
740-
const functionTools = agent.tools.filter(isFunctionTool) as FunctionTool[];
740+
const functionTools = agent.tools.filter(isFunctionTool);
741741
const toolsByName = new Map(functionTools.map((t) => [t.name, t]));
742742

743743
const results = await Promise.all(

0 commit comments

Comments
 (0)