Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/runtime/modules/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,14 +1298,20 @@ export async function findAgentByName(name: string, env: Environment): Promise<A

export async function findProviderForLLM(
llmName: string,
env: Environment
_env: Environment
): Promise<AgentServiceProvider> {
// LLM rows are upserted during startup via runStandaloneStatements on GlobalEnvironment.
// Agent invocations often run under a child env (e.g. exec-graph sets active user from the
// event). Querying with that env can use a different tenant than bootstrap, so the LLM row
// is invisible. Use a child of GlobalEnvironment so lookup matches inserted configuration.
const lookupEnv = new Environment('llm-provider-lookup', GlobalEnvironment);

let p: AgentServiceProvider | undefined = ProviderDb.get(llmName);
if (p === undefined) {
const result: Instance[] = await parseAndEvaluateStatement(
`{${CoreAIModuleName}/${LlmEntityName} {name? "${llmName}"}}`,
undefined,
env
lookupEnv
);
if (result.length > 0) {
const llm: Instance = result[0];
Expand All @@ -1324,7 +1330,10 @@ export async function findProviderForLLM(
if (p) {
return p;
} else {
throw new Error(`Failed to load provider for ${llmName}`);
throw new Error(
`Failed to load provider for ${llmName}: no agentlang.ai/LLM row named "${llmName}". ` +
`Define it with {agentlang.ai/LLM { name "${llmName}", service "openai"|"anthropic", ... }, @upsert} in a loaded module (not config.al, which is excluded from automatic loading), or add it under agentlang.ai in agentlang config JSON.`
);
}
}

Expand Down
66 changes: 65 additions & 1 deletion test/runtime/llm-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { beforeAll, describe, expect, test } from 'vitest';
import { doInternModule } from '../util.js';
import { GlobalEnvironment, parseAndEvaluateStatement } from '../../src/runtime/interpreter.js';
import { Environment, GlobalEnvironment, parseAndEvaluateStatement } from '../../src/runtime/interpreter.js';
import { setLocalEnv } from '../../src/runtime/auth/defs.js';
import { findProviderForLLM } from '../../src/runtime/modules/ai.js';

describe('LLM Service Selection', () => {
beforeAll(() => {
Expand Down Expand Up @@ -207,4 +208,67 @@ describe('LLM Service Selection', () => {
const config = llm.lookup('config');
expect(config.get ? config.get('model') : config['model']).toBe('claude-3-sonnet-20241022');
});

test('named LLM + @public myHelloAgent: provider resolve and live invoke when API key is set', async () => {
const envKey = process.env.AGENTLANG_ANTHROPIC_KEY;
const useLiveAnthropic = Boolean(
envKey && envKey !== 'test-anthropic-key' && envKey.length > 12
);
if (useLiveAnthropic) {
setLocalEnv('AGENTLANG_ANTHROPIC_KEY', envKey!);
}

await doInternModule(
'TestClaudeNamedLLM',
`
{agentlang.ai/LLM {
name "claude",
service "anthropic",
config {
"model": "claude-sonnet-4-20250514",
"max_tokens": 4096,
"stream": false
}
}, @upsert}

@public agent myHelloAgent {
llm "claude",
role "You are to greet me with hello"
}
`
);

const childEnv = new Environment('jwt-like-child', GlobalEnvironment);
childEnv.setActiveUser('some-end-user-id');

const provider = await findProviderForLLM('claude', childEnv);
expect(provider).toBeDefined();

if (useLiveAnthropic) {
const response = await parseAndEvaluateStatement(
`{TestClaudeNamedLLM/myHelloAgent {message "Please greet me in one short sentence."}}`,
undefined,
GlobalEnvironment
);
expect(typeof response).toBe('string');
expect((response as string).length).toBeGreaterThan(0);
expect((response as string).toLowerCase()).toMatch(/hello|hi|hey/);
}
});

test('findProviderForLLM fails clearly when LLM entity was never upserted', async () => {
await doInternModule(
'TestMissingLLMRow',
`
@public agent orphanAgent {
llm "no_such_llm_registered",
instruction "test"
}
`
);

await expect(findProviderForLLM('no_such_llm_registered', GlobalEnvironment)).rejects.toThrow(
/no agentlang\.ai\/LLM row/
);
});
});
Loading