Skip to content

Commit b14f089

Browse files
committed
feat(js/plugins/anthropic): lazily calculate API key at request time
1 parent c47ee23 commit b14f089

File tree

11 files changed

+300
-68
lines changed

11 files changed

+300
-68
lines changed

js/plugins/anthropic/src/index.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,24 +44,38 @@ export type { AnthropicCacheControl, AnthropicCitation } from './types.js';
4444
export { cacheControl } from './utils.js';
4545

4646
/**
47-
* Gets or creates an Anthropic client instance.
48-
* Supports test client injection for internal testing.
47+
* Gets the test client if injected (for testing only).
48+
* @internal
4949
*/
50-
function getAnthropicClient(options?: PluginOptions): Anthropic {
51-
// Check for test client injection first (internal use only)
50+
function getTestClient(options?: PluginOptions): Anthropic | undefined {
5251
const internalOptions = options as InternalPluginOptions | undefined;
53-
if (internalOptions?.[__testClient]) {
54-
return internalOptions[__testClient];
52+
return internalOptions?.[__testClient];
53+
}
54+
55+
/**
56+
* Validates the API key configuration at plugin initialization.
57+
* When apiKey is false, validation is deferred to request time.
58+
*
59+
* @throws Error if API key is required but not available
60+
*/
61+
function validateApiKey(options?: PluginOptions): void {
62+
// If apiKey is explicitly false, defer validation to request time
63+
if (options?.apiKey === false) {
64+
return;
5565
}
5666

57-
// Production path: create real client
67+
// Check for test client injection (for testing only)
68+
if (getTestClient(options)) {
69+
return;
70+
}
71+
72+
// Validate that we have an API key available
5873
const apiKey = options?.apiKey || process.env.ANTHROPIC_API_KEY;
5974
if (!apiKey) {
6075
throw new Error(
6176
'Please pass in the API key or set the ANTHROPIC_API_KEY environment variable'
6277
);
6378
}
64-
return new Anthropic({ apiKey });
6579
}
6680

6781
/**
@@ -94,8 +108,12 @@ function getAnthropicClient(options?: PluginOptions): Anthropic {
94108
* ```
95109
*/
96110
function anthropicPlugin(options?: PluginOptions): GenkitPluginV2 {
97-
const client = getAnthropicClient(options);
111+
// Validate API key at plugin init (unless deferred with apiKey: false)
112+
validateApiKey(options);
113+
114+
const pluginApiKey = options?.apiKey;
98115
const defaultApiVersion = options?.apiVersion;
116+
const testClient = getTestClient(options);
99117

100118
let listActionsCache: ActionMetadata[] | null = null;
101119

@@ -106,8 +124,9 @@ function anthropicPlugin(options?: PluginOptions): GenkitPluginV2 {
106124
for (const name of Object.keys(KNOWN_CLAUDE_MODELS)) {
107125
const action = claudeModel({
108126
name,
109-
client,
127+
pluginApiKey,
110128
defaultApiVersion,
129+
testClient,
111130
});
112131
actions.push(action);
113132
}
@@ -119,14 +138,25 @@ function anthropicPlugin(options?: PluginOptions): GenkitPluginV2 {
119138
const modelName = name.startsWith('anthropic/') ? name.slice(10) : name;
120139
return claudeModel({
121140
name: modelName,
122-
client,
141+
pluginApiKey,
123142
defaultApiVersion,
143+
testClient,
124144
});
125145
}
126146
return undefined;
127147
},
128148
list: async () => {
129149
if (listActionsCache) return listActionsCache;
150+
// For listing, we need a client. Create one if we have an API key available.
151+
const listApiKey =
152+
pluginApiKey !== false
153+
? pluginApiKey || process.env.ANTHROPIC_API_KEY
154+
: undefined;
155+
if (!listApiKey && !testClient) {
156+
// Can't list models without an API key
157+
return [];
158+
}
159+
const client = testClient ?? new Anthropic({ apiKey: listApiKey });
130160
listActionsCache = await listActions(client);
131161
return listActionsCache;
132162
},

js/plugins/anthropic/src/models.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18+
import Anthropic from '@anthropic-ai/sdk';
1819
import type {
1920
GenerateRequest,
2021
GenerateResponseData,
@@ -33,6 +34,7 @@ import {
3334
AnthropicBaseConfigSchemaType,
3435
AnthropicConfigSchema,
3536
AnthropicThinkingConfigSchema,
37+
calculateApiKey,
3638
resolveBetaEnabled,
3739
type ClaudeModelParams,
3840
type ClaudeRunnerParams,
@@ -198,14 +200,7 @@ export function claudeRunner<TConfigSchema extends z.ZodTypeAny>(
198200
params: ClaudeRunnerParams,
199201
configSchema: TConfigSchema
200202
) {
201-
const { defaultApiVersion, ...runnerParams } = params;
202-
203-
if (!runnerParams.client) {
204-
throw new Error('Anthropic client is required to create a runner');
205-
}
206-
207-
let stableRunner: Runner | null = null;
208-
let betaRunner: BetaRunner | null = null;
203+
const { defaultApiVersion, pluginApiKey, testClient, name } = params;
209204

210205
return async (
211206
request: GenerateRequest<TConfigSchema>,
@@ -223,13 +218,28 @@ export function claudeRunner<TConfigSchema extends z.ZodTypeAny>(
223218
const normalizedRequest = request as unknown as GenerateRequest<
224219
typeof AnthropicConfigSchema
225220
>;
221+
222+
// Determine the client to use
223+
// Test client takes precedence, otherwise calculate API key at request time
224+
const client = testClient
225+
? testClient
226+
: new Anthropic({
227+
apiKey: calculateApiKey(
228+
pluginApiKey,
229+
normalizedRequest.config?.apiKey
230+
),
231+
});
232+
226233
const isBeta = resolveBetaEnabled(
227234
normalizedRequest.config,
228235
defaultApiVersion
229236
);
237+
238+
// Create runner with the client
230239
const runner = isBeta
231-
? (betaRunner ??= new BetaRunner(runnerParams))
232-
: (stableRunner ??= new Runner(runnerParams));
240+
? new BetaRunner({ name, client })
241+
: new Runner({ name, client });
242+
233243
return runner.run(normalizedRequest, {
234244
streamingRequested,
235245
sendChunk,
@@ -265,17 +275,22 @@ export function claudeModelReference(
265275
}
266276

267277
/**
268-
* Defines a Claude model with the given name and Anthropic client.
278+
* Defines a Claude model with the given name and API key configuration.
269279
* Accepts any model name and lets the API validate it. If the model is in KNOWN_CLAUDE_MODELS, uses that modelRef
270280
* for better defaults; otherwise creates a generic model reference.
271281
*/
272282
export function claudeModel(
273283
params: ClaudeModelParams
274284
): ModelAction<z.ZodTypeAny> {
275-
const { name, client: runnerClient, defaultApiVersion: apiVersion } = params;
285+
const {
286+
name,
287+
pluginApiKey,
288+
defaultApiVersion: apiVersion,
289+
testClient,
290+
} = params;
276291
// Use supported model ref if available, otherwise create generic model ref
277292
const knownModelRef = KNOWN_CLAUDE_MODELS[name];
278-
let modelInfo = knownModelRef
293+
const modelInfo = knownModelRef
279294
? knownModelRef.info
280295
: GENERIC_CLAUDE_MODEL_INFO;
281296
const configSchema = knownModelRef?.configSchema ?? AnthropicConfigSchema;
@@ -291,8 +306,9 @@ export function claudeModel(
291306
claudeRunner(
292307
{
293308
name,
294-
client: runnerClient,
309+
pluginApiKey,
295310
defaultApiVersion: apiVersion,
311+
testClient,
296312
},
297313
configSchema
298314
)

js/plugins/anthropic/src/runner/base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
MediaSchema,
3434
MediaType,
3535
MediaTypeSchema,
36-
type ClaudeRunnerParams,
36+
type RunnerConstructorParams,
3737
type ThinkingConfig,
3838
} from '../types.js';
3939

@@ -66,7 +66,7 @@ export abstract class BaseRunner<ApiTypes extends RunnerTypes> {
6666
*/
6767
protected readonly DEFAULT_MAX_OUTPUT_TOKENS = 4096;
6868

69-
constructor(params: ClaudeRunnerParams) {
69+
constructor(params: RunnerConstructorParams) {
7070
this.name = params.name;
7171
this.client = params.client;
7272
}

js/plugins/anthropic/src/runner/beta.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js';
4646
import {
4747
AnthropicConfigSchema,
4848
type AnthropicDocumentOptions,
49-
type ClaudeRunnerParams,
49+
type RunnerConstructorParams,
5050
} from '../types.js';
5151
import { removeUndefinedProperties } from '../utils.js';
5252
import { BaseRunner } from './base.js';
@@ -142,7 +142,7 @@ interface BetaRunnerTypes extends RunnerTypes {
142142
* Runner for the Anthropic Beta API.
143143
*/
144144
export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
145-
constructor(params: ClaudeRunnerParams) {
145+
constructor(params: RunnerConstructorParams) {
146146
super(params);
147147
}
148148

@@ -318,12 +318,13 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
318318
// Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
319319
// This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
320320
// Thinking is extracted separately to avoid type issues.
321-
// ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
321+
// apiVersion and apiKey are extracted separately as they're not valid properties for the Anthropic API.
322322
const {
323323
topP,
324324
topK,
325325
apiVersion: _1,
326326
thinking: _2,
327+
apiKey: _3,
327328
...restConfig
328329
} = request.config ?? {};
329330

@@ -375,12 +376,13 @@ export class BetaRunner extends BaseRunner<BetaRunnerTypes> {
375376
// Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
376377
// This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
377378
// Thinking is extracted separately to avoid type issues.
378-
// ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
379+
// apiVersion and apiKey are extracted separately as they're not valid properties for the Anthropic API.
379380
const {
380381
topP,
381382
topK,
382383
apiVersion: _1,
383384
thinking: _2,
385+
apiKey: _3,
384386
...restConfig
385387
} = request.config ?? {};
386388

js/plugins/anthropic/src/runner/stable.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { KNOWN_CLAUDE_MODELS, extractVersion } from '../models.js';
4444
import {
4545
AnthropicConfigSchema,
4646
type AnthropicDocumentOptions,
47-
type ClaudeRunnerParams,
47+
type RunnerConstructorParams,
4848
} from '../types.js';
4949
import { removeUndefinedProperties } from '../utils.js';
5050
import { BaseRunner } from './base.js';
@@ -85,7 +85,7 @@ interface RunnerTypes extends BaseRunnerTypes {
8585
}
8686

8787
export class Runner extends BaseRunner<RunnerTypes> {
88-
constructor(params: ClaudeRunnerParams) {
88+
constructor(params: RunnerConstructorParams) {
8989
super(params);
9090
}
9191

@@ -237,12 +237,13 @@ export class Runner extends BaseRunner<RunnerTypes> {
237237
// Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
238238
// This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
239239
// Thinking is extracted separately to avoid type issues.
240-
// ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
240+
// apiVersion and apiKey are extracted separately as they're not valid properties for the Anthropic API.
241241
const {
242242
topP,
243243
topK,
244244
apiVersion: _1,
245245
thinking: _2,
246+
apiKey: _3,
246247
...restConfig
247248
} = request.config ?? {};
248249

@@ -288,12 +289,13 @@ export class Runner extends BaseRunner<RunnerTypes> {
288289
// Need to extract topP and topK from request.config to avoid duplicate properties being added to the body
289290
// This happens because topP and topK have different property names (top_p and top_k) in the Anthropic API.
290291
// Thinking is extracted separately to avoid type issues.
291-
// ApiVersion is extracted separately as it's not a valid property for the Anthropic API.
292+
// apiVersion and apiKey are extracted separately as they're not valid properties for the Anthropic API.
292293
const {
293294
topP,
294295
topK,
295296
apiVersion: _1,
296297
thinking: _2,
298+
apiKey: _3,
297299
...restConfig
298300
} = request.config ?? {};
299301

0 commit comments

Comments
 (0)