Repository navigation
feat(runtime-core,blocks): resolve the agent block provider instead of hardcoding OpenAI #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
10b898d
feat(runtime-core,blocks): resolve the agent block provider instead o…
jamesbhobbs 296dcda
Merge remote-tracking branch 'origin/main' into feat/agent-provider-r…
jamesbhobbs c98cca3
fix(runtime-core): realign the lockfile with main's undici override
jamesbhobbs b9b3ef4
Merge branch 'main' into feat/agent-provider-registry
dinohamzic 75328b7
chore(runtime-core): upgrade agent providers to AI SDK 7
dinohamzic 8efb092
chore: refresh agent model examples and defaults
dinohamzic 6faa533
fix(runtime-core): resolve provider credentials and model capabilities
dinohamzic 42ea6e9
docs: simplify agent provider setup and errors
dinohamzic 9b0a441
Merge branch 'main' into feat/agent-provider-registry
dinohamzic ab3483f
fix(runtime-core): keep agent API keys with their provider
dinohamzic 25b00bd
fix(runtime-core): accept ANTHROPIC_BASE_URL with or without /v1
dinohamzic cadfa20
refactor(runtime-core): simplify agent provider resolution
dinohamzic 565962f
docs(examples): point agent block setup at the provider docs
dinohamzic 472fbaf
fix(runtime-core): surface provider errors from agent streams
dinohamzic 4b8bf66
fix(cli): separate agent text from different steps
dinohamzic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import { describe, expect, it } from 'vitest' | ||
| import { apiKeyEnvVarFor, parseAgentModel, resolveAgentModel } from './agent-provider' | ||
|
|
||
| describe('parseAgentModel', () => { | ||
| it('defaults to openai/auto when no model is set', () => { | ||
| expect(parseAgentModel(undefined)).toEqual({ providerId: 'openai', modelName: 'auto' }) | ||
| expect(parseAgentModel('')).toEqual({ providerId: 'openai', modelName: 'auto' }) | ||
| expect(parseAgentModel(' ')).toEqual({ providerId: 'openai', modelName: 'auto' }) | ||
| }) | ||
|
|
||
| it('treats a bare model name as openai, so existing files keep working', () => { | ||
| expect(parseAgentModel('auto')).toEqual({ providerId: 'openai', modelName: 'auto' }) | ||
| expect(parseAgentModel('gpt-5')).toEqual({ providerId: 'openai', modelName: 'gpt-5' }) | ||
| expect(parseAgentModel('gpt-5.6-sol')).toEqual({ providerId: 'openai', modelName: 'gpt-5.6-sol' }) | ||
| }) | ||
|
|
||
| it('splits a known provider prefix', () => { | ||
| expect(parseAgentModel('anthropic:claude-opus-5')).toEqual({ | ||
| providerId: 'anthropic', | ||
| modelName: 'claude-opus-5', | ||
| }) | ||
| expect(parseAgentModel('openai:gpt-5')).toEqual({ providerId: 'openai', modelName: 'gpt-5' }) | ||
| expect(parseAgentModel('openai-compatible:llama3')).toEqual({ | ||
| providerId: 'openai-compatible', | ||
| modelName: 'llama3', | ||
| }) | ||
| }) | ||
|
|
||
| it('leaves slash-separated aggregator ids intact', () => { | ||
| expect(parseAgentModel('openai-compatible:anthropic/claude-opus-5')).toEqual({ | ||
| providerId: 'openai-compatible', | ||
| modelName: 'anthropic/claude-opus-5', | ||
| }) | ||
| }) | ||
|
|
||
| it('keeps an unknown prefix as part of the model name', () => { | ||
| // A colon is legal in a model name; only a known provider id is a prefix. | ||
| expect(parseAgentModel('mistral:large')).toEqual({ providerId: 'openai', modelName: 'mistral:large' }) | ||
| }) | ||
|
|
||
| it('falls back to auto when a provider prefix has no model', () => { | ||
| expect(parseAgentModel('anthropic:')).toEqual({ providerId: 'anthropic', modelName: 'auto' }) | ||
| }) | ||
|
|
||
| it('ignores a leading colon rather than reading an empty provider', () => { | ||
| expect(parseAgentModel(':gpt-5')).toEqual({ providerId: 'openai', modelName: ':gpt-5' }) | ||
| }) | ||
|
|
||
| it('trims surrounding whitespace', () => { | ||
| expect(parseAgentModel(' anthropic:claude-opus-5 ')).toEqual({ | ||
| providerId: 'anthropic', | ||
| modelName: 'claude-opus-5', | ||
| }) | ||
| }) | ||
| }) | ||
|
|
||
| describe('apiKeyEnvVarFor', () => { | ||
| it('names the variable each provider reads', () => { | ||
| expect(apiKeyEnvVarFor('openai')).toBe('OPENAI_API_KEY') | ||
| expect(apiKeyEnvVarFor('anthropic')).toBe('ANTHROPIC_API_KEY') | ||
| expect(apiKeyEnvVarFor('openai-compatible')).toBe('DEEPNOTE_AGENT_API_KEY') | ||
| }) | ||
| }) | ||
|
|
||
| describe('resolveAgentModel', () => { | ||
| it('keeps the documented openai default and reasoning summaries', () => { | ||
| const resolved = resolveAgentModel({ spec: 'auto', apiKey: 'k', env: {} }) | ||
|
|
||
| expect(resolved.providerId).toBe('openai') | ||
| expect(resolved.modelName).toBe('gpt-5') | ||
| expect(resolved.providerOptions).toEqual({ openai: { reasoningSummary: 'auto' } }) | ||
| }) | ||
|
|
||
| it('lets OPENAI_MODEL override the openai default', () => { | ||
| const resolved = resolveAgentModel({ spec: 'auto', apiKey: 'k', env: { OPENAI_MODEL: 'gpt-4o' } }) | ||
|
|
||
| expect(resolved.modelName).toBe('gpt-4o') | ||
| }) | ||
|
|
||
| it('prefers the block model over OPENAI_MODEL', () => { | ||
| const resolved = resolveAgentModel({ spec: 'gpt-5', apiKey: 'k', env: { OPENAI_MODEL: 'gpt-4o' } }) | ||
|
|
||
| expect(resolved.modelName).toBe('gpt-5') | ||
| }) | ||
|
|
||
| it('drops reasoning summaries when OPENAI_BASE_URL points elsewhere', () => { | ||
| // Most OpenAI-compatible endpoints have no Responses API, so the handler | ||
| // falls back to Chat Completions and the option would be rejected. | ||
| const resolved = resolveAgentModel({ | ||
| spec: 'auto', | ||
| apiKey: 'k', | ||
| env: { OPENAI_BASE_URL: 'https://example.test/v1' }, | ||
| }) | ||
|
|
||
| expect(resolved.providerOptions).toEqual({}) | ||
| }) | ||
|
|
||
| it('resolves anthropic with a Claude default and summarized thinking', () => { | ||
| const resolved = resolveAgentModel({ spec: 'anthropic:auto', apiKey: 'k', env: {} }) | ||
|
|
||
| expect(resolved.providerId).toBe('anthropic') | ||
| expect(resolved.modelName).toBe('claude-opus-5') | ||
| expect(resolved.providerOptions).toEqual({ | ||
| anthropic: { thinking: { type: 'adaptive', display: 'summarized' } }, | ||
| }) | ||
| }) | ||
|
|
||
| it('lets ANTHROPIC_MODEL override the anthropic default', () => { | ||
| const resolved = resolveAgentModel({ | ||
| spec: 'anthropic:auto', | ||
| apiKey: 'k', | ||
| env: { ANTHROPIC_MODEL: 'claude-sonnet-5' }, | ||
| }) | ||
|
|
||
| expect(resolved.modelName).toBe('claude-sonnet-5') | ||
| }) | ||
|
|
||
| it('does not let OPENAI_MODEL leak into another provider', () => { | ||
| const resolved = resolveAgentModel({ spec: 'anthropic:auto', apiKey: 'k', env: { OPENAI_MODEL: 'gpt-4o' } }) | ||
|
|
||
| expect(resolved.modelName).toBe('claude-opus-5') | ||
| }) | ||
|
|
||
| it('resolves openai-compatible from its own variables', () => { | ||
| const resolved = resolveAgentModel({ | ||
| spec: 'openai-compatible:auto', | ||
| apiKey: 'k', | ||
| env: { DEEPNOTE_AGENT_BASE_URL: 'https://openrouter.ai/api/v1', DEEPNOTE_AGENT_MODEL: 'anthropic/claude-opus-5' }, | ||
| }) | ||
|
|
||
| expect(resolved.modelName).toBe('anthropic/claude-opus-5') | ||
| expect(resolved.providerOptions).toEqual({}) | ||
| }) | ||
|
|
||
| it('falls back to the OPENAI_* variables for openai-compatible', () => { | ||
| const resolved = resolveAgentModel({ | ||
| spec: 'openai-compatible:auto', | ||
| apiKey: 'k', | ||
| env: { OPENAI_BASE_URL: 'http://localhost:11434/v1', OPENAI_MODEL: 'llama3' }, | ||
| }) | ||
|
|
||
| expect(resolved.modelName).toBe('llama3') | ||
| }) | ||
|
|
||
| it('names the variable to set when openai-compatible has no endpoint', () => { | ||
| expect(() => resolveAgentModel({ spec: 'openai-compatible:llama3', apiKey: 'k', env: {} })).toThrow( | ||
| /DEEPNOTE_AGENT_BASE_URL/ | ||
| ) | ||
| }) | ||
|
|
||
| it('refuses to guess a model for openai-compatible', () => { | ||
| expect(() => | ||
| resolveAgentModel({ | ||
| spec: 'openai-compatible:auto', | ||
| apiKey: 'k', | ||
| env: { DEEPNOTE_AGENT_BASE_URL: 'https://example.test/v1' }, | ||
| }) | ||
| ).toThrow(/DEEPNOTE_AGENT_MODEL/) | ||
| }) | ||
|
|
||
| it('ignores empty env values instead of treating them as configured', () => { | ||
| const resolved = resolveAgentModel({ spec: 'auto', apiKey: 'k', env: { OPENAI_MODEL: '' } }) | ||
|
|
||
| expect(resolved.modelName).toBe('gpt-5') | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.