@aragon-agent/core 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/dist/engine/agent-loop.js +5 -1
- package/dist/llm/context-window.d.ts +9 -0
- package/dist/llm/context-window.js +46 -0
- package/dist/llm/model-discovery.d.ts +5 -0
- package/dist/llm/model-discovery.js +18 -0
- package/dist/llm/model-registry.d.ts +6 -1
- package/dist/llm/model-registry.js +66 -28
- package/dist/llm/provider.d.ts +1 -1
- package/dist/llm/providers/anthropic.d.ts +1 -1
- package/dist/llm/providers/anthropic.js +37 -13
- package/dist/llm/providers/google.d.ts +1 -1
- package/dist/llm/providers/google.js +35 -9
- package/dist/llm/providers/openai.d.ts +1 -1
- package/dist/llm/providers/openai.js +13 -3
- package/dist/llm/types.d.ts +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,18 @@ All notable changes to `@aragon-agent/core` are documented here. Entries at
|
|
|
5
5
|
— those releases really were published under that name, so they are left as they
|
|
6
6
|
were written.
|
|
7
7
|
|
|
8
|
-
## Unreleased
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Model context resolution now reads explicit OpenAI-compatible, Anthropic and
|
|
13
|
+
Google API limits, preserves their provenance, and resolves endpoint-scoped
|
|
14
|
+
discovery before catalog/alias fallbacks. Unknown limits stay marked as such.
|
|
15
|
+
Context metadata does not imply known pricing.
|
|
16
|
+
- Google/Anthropic discovery follows bounded pagination under a single 15-second
|
|
17
|
+
deadline. All three adapters accept cancellation; cancelled or invalidated
|
|
18
|
+
lookups cannot overwrite current metadata. Clearing one provider's cache leaves
|
|
19
|
+
other providers' pending discoveries intact.
|
|
9
20
|
|
|
10
21
|
### Added
|
|
11
22
|
|
|
@@ -292,7 +292,9 @@ export async function runAgentLoop(ctx) {
|
|
|
292
292
|
// 3. INSIDE the `while`, not before it. A run that starts under the
|
|
293
293
|
// threshold and crosses it at turn 12 is the normal case, and it is the
|
|
294
294
|
// case an `agent_end`-based design cannot serve at all (D-1).
|
|
295
|
-
await runCompaction(ctx, { trigger: 'pressure', lastUsage, turnIndex })
|
|
295
|
+
if (await runCompaction(ctx, { trigger: 'pressure', lastUsage, turnIndex })) {
|
|
296
|
+
lastUsage = undefined;
|
|
297
|
+
}
|
|
296
298
|
if (ctx.signal.aborted)
|
|
297
299
|
break;
|
|
298
300
|
// ----- Turn start -----
|
|
@@ -351,6 +353,8 @@ export async function runAgentLoop(ctx) {
|
|
|
351
353
|
throw err;
|
|
352
354
|
overflowRecovered = true;
|
|
353
355
|
const recovered = await runCompaction(ctx, { trigger: 'overflow', lastUsage, turnIndex });
|
|
356
|
+
if (recovered)
|
|
357
|
+
lastUsage = undefined;
|
|
354
358
|
if (!recovered)
|
|
355
359
|
throw err;
|
|
356
360
|
if (ctx.signal.aborted)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ModelInfo } from './types.js';
|
|
2
|
+
export type ContextWindowInfo = Pick<ModelInfo, 'contextWindow'> & {
|
|
3
|
+
contextWindowSource: 'api' | 'catalog' | 'fallback';
|
|
4
|
+
};
|
|
5
|
+
/** Never interpret output-token limits as an input/context limit. */
|
|
6
|
+
export declare function reportedContextWindow(model: Record<string, unknown>): ContextWindowInfo;
|
|
7
|
+
/** Recognize only explicit vendor namespaces and dated snapshots, not arbitrary aliases. */
|
|
8
|
+
export declare function catalogModelId(modelId: string): string;
|
|
9
|
+
export declare function catalogContextWindow(modelId: string): number | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Never interpret output-token limits as an input/context limit. */
|
|
2
|
+
export function reportedContextWindow(model) {
|
|
3
|
+
const top = model.top_provider;
|
|
4
|
+
const candidates = [top?.context_length, model.context_length, model.context_window,
|
|
5
|
+
model.max_input_tokens, model.inputTokenLimit, model.contextWindow];
|
|
6
|
+
const value = candidates.find((n) => typeof n === 'number' && Number.isSafeInteger(n) && n > 0);
|
|
7
|
+
return typeof value === 'number'
|
|
8
|
+
? { contextWindow: value, contextWindowSource: 'api' }
|
|
9
|
+
: { contextWindow: 128_000, contextWindowSource: 'fallback' };
|
|
10
|
+
}
|
|
11
|
+
/** Recognize only explicit vendor namespaces and dated snapshots, not arbitrary aliases. */
|
|
12
|
+
export function catalogModelId(modelId) {
|
|
13
|
+
return modelId.trim().toLowerCase()
|
|
14
|
+
.replace(/^(models|openai|anthropic|google)\//, '')
|
|
15
|
+
.replace(/-(?:\d{4}-\d{2}-\d{2}|\d{8})$/, '');
|
|
16
|
+
}
|
|
17
|
+
// Context-only fallbacks: do not invent prices or change the model picker.
|
|
18
|
+
// https://developers.openai.com/api/docs/models/gpt-5
|
|
19
|
+
// https://developers.openai.com/api/docs/models/gpt-5.1
|
|
20
|
+
// https://developers.openai.com/api/docs/models/gpt-5.2
|
|
21
|
+
// https://developers.openai.com/api/docs/models/gpt-5.4
|
|
22
|
+
// https://developers.openai.com/api/docs/models/gpt-5.4-mini
|
|
23
|
+
// https://developers.openai.com/api/docs/models/gpt-5-mini
|
|
24
|
+
// https://developers.openai.com/api/docs/models/gpt-5.3-codex
|
|
25
|
+
// https://developers.openai.com/api/docs/models/gpt-4.1
|
|
26
|
+
// https://ai.google.dev/gemini-api/docs/models/gemini-2.5-pro
|
|
27
|
+
// https://ai.google.dev/gemini-api/docs/models/gemini-2.5-flash
|
|
28
|
+
// https://platform.claude.com/docs/en/models/sonnet-4-6/overview
|
|
29
|
+
// Explicit IDs avoid assuming that future versions or chat variants share a limit.
|
|
30
|
+
const CATALOG = {
|
|
31
|
+
'gpt-5': 400_000,
|
|
32
|
+
'gpt-5.1': 400_000,
|
|
33
|
+
'gpt-5.2': 400_000,
|
|
34
|
+
'gpt-5.4': 1_050_000,
|
|
35
|
+
'gpt-5.4-mini': 400_000,
|
|
36
|
+
'gpt-5-mini': 400_000,
|
|
37
|
+
'gpt-5.3-codex': 400_000,
|
|
38
|
+
'gpt-4.1': 1_047_576,
|
|
39
|
+
'gemini-2.5-pro': 1_048_576,
|
|
40
|
+
'gemini-2.5-flash': 1_048_576,
|
|
41
|
+
'claude-sonnet-4-6': 1_000_000,
|
|
42
|
+
};
|
|
43
|
+
export function catalogContextWindow(modelId) {
|
|
44
|
+
const id = catalogModelId(modelId);
|
|
45
|
+
return Object.prototype.hasOwnProperty.call(CATALOG, id) ? CATALOG[id] : undefined;
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** One bounded lifetime for all pages, also cancelled when the owning session ends. */
|
|
2
|
+
export function createModelDiscoverySignal(parent) {
|
|
3
|
+
const controller = new AbortController();
|
|
4
|
+
const abort = () => controller.abort(parent?.reason);
|
|
5
|
+
const timer = setTimeout(() => controller.abort(new Error('Model discovery timed out')), 15_000);
|
|
6
|
+
timer.unref();
|
|
7
|
+
if (parent?.aborted)
|
|
8
|
+
abort();
|
|
9
|
+
else
|
|
10
|
+
parent?.addEventListener('abort', abort, { once: true });
|
|
11
|
+
return {
|
|
12
|
+
signal: controller.signal,
|
|
13
|
+
dispose: () => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
parent?.removeEventListener('abort', abort);
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ModelInfo } from './types.js';
|
|
10
10
|
import type { ProviderRegistry } from './providers/index.js';
|
|
11
|
+
import { type ContextWindowInfo } from './context-window.js';
|
|
11
12
|
export declare class ModelRegistry {
|
|
12
13
|
/** Static/builtin models keyed by provider id. */
|
|
13
14
|
private readonly builtinModels;
|
|
14
15
|
/** API-discovered models cache keyed by `${providerId}:${baseUrl}`. */
|
|
15
16
|
private readonly discoveryCache;
|
|
17
|
+
private readonly pendingDiscovery;
|
|
16
18
|
/** Optional reference to the provider registry for API discovery. */
|
|
17
19
|
private providerRegistry;
|
|
18
20
|
constructor(providerRegistry?: ProviderRegistry);
|
|
@@ -22,12 +24,15 @@ export declare class ModelRegistry {
|
|
|
22
24
|
getModels(providerId: string): ModelInfo[];
|
|
23
25
|
/** Look up a specific model by provider + model id. */
|
|
24
26
|
getModel(providerId: string, modelId: string): ModelInfo | undefined;
|
|
27
|
+
/** Resolve context separately from price, scoped to the configured endpoint. */
|
|
28
|
+
getContextWindow(providerId: string, modelId: string, baseUrl?: string): ContextWindowInfo;
|
|
29
|
+
private cacheKey;
|
|
25
30
|
/**
|
|
26
31
|
* Discover models from the provider's API. Results are cached for 5 min.
|
|
27
32
|
*
|
|
28
33
|
* Falls back to an empty array on failure — never throws.
|
|
29
34
|
*/
|
|
30
|
-
discoverModels(providerId: string, apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
35
|
+
discoverModels(providerId: string, apiKey: string, baseUrl?: string, signal?: AbortSignal): Promise<ModelInfo[]>;
|
|
31
36
|
/** Invalidate the discovery cache for a provider (or all). */
|
|
32
37
|
clearCache(providerId?: string): void;
|
|
33
38
|
/**
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* new LLMProvider interface for API queries.
|
|
8
8
|
*/
|
|
9
9
|
import { DEFAULT_MAX_OUTPUT_TOKENS, learnModelCeiling } from './output-limits.js';
|
|
10
|
+
import { catalogContextWindow, catalogModelId } from './context-window.js';
|
|
10
11
|
/** Cache TTL: 5 minutes. */
|
|
11
12
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +18,7 @@ export class ModelRegistry {
|
|
|
17
18
|
builtinModels = new Map();
|
|
18
19
|
/** API-discovered models cache keyed by `${providerId}:${baseUrl}`. */
|
|
19
20
|
discoveryCache = new Map();
|
|
21
|
+
pendingDiscovery = new Map();
|
|
20
22
|
/** Optional reference to the provider registry for API discovery. */
|
|
21
23
|
providerRegistry;
|
|
22
24
|
constructor(providerRegistry) {
|
|
@@ -39,54 +41,87 @@ export class ModelRegistry {
|
|
|
39
41
|
const models = this.builtinModels.get(providerId);
|
|
40
42
|
return models?.find((m) => m.id === modelId);
|
|
41
43
|
}
|
|
44
|
+
/** Resolve context separately from price, scoped to the configured endpoint. */
|
|
45
|
+
getContextWindow(providerId, modelId, baseUrl) {
|
|
46
|
+
const discovered = this.discoveryCache.get(this.cacheKey(providerId, baseUrl))?.models
|
|
47
|
+
.find((m) => m.id.replace(/^models\//, '') === modelId.replace(/^models\//, ''));
|
|
48
|
+
if (discovered?.contextWindowSource === 'api' &&
|
|
49
|
+
Number.isSafeInteger(discovered.contextWindow) && discovered.contextWindow > 0) {
|
|
50
|
+
return { contextWindow: discovered.contextWindow, contextWindowSource: 'api' };
|
|
51
|
+
}
|
|
52
|
+
const id = catalogModelId(modelId);
|
|
53
|
+
const builtin = this.getModel(providerId, modelId) ?? this.getModels(providerId)
|
|
54
|
+
.find((m) => catalogModelId(m.id) === id) ?? [...this.builtinModels.values()]
|
|
55
|
+
.flat().find((m) => catalogModelId(m.id) === id);
|
|
56
|
+
const builtinWindow = builtin?.contextWindowSource !== 'fallback' &&
|
|
57
|
+
Number.isSafeInteger(builtin?.contextWindow) && (builtin?.contextWindow ?? 0) > 0
|
|
58
|
+
? builtin?.contextWindow : undefined;
|
|
59
|
+
const window = builtinWindow ?? catalogContextWindow(id);
|
|
60
|
+
return window !== undefined
|
|
61
|
+
? { contextWindow: window, contextWindowSource: 'catalog' }
|
|
62
|
+
: { contextWindow: 128_000, contextWindowSource: 'fallback' };
|
|
63
|
+
}
|
|
64
|
+
cacheKey(providerId, baseUrl) {
|
|
65
|
+
const url = baseUrl || this.providerRegistry?.get(providerId)?.defaultBaseUrl || '';
|
|
66
|
+
return `${providerId}:${url.replace(/\/+$/, '')}`;
|
|
67
|
+
}
|
|
42
68
|
/**
|
|
43
69
|
* Discover models from the provider's API. Results are cached for 5 min.
|
|
44
70
|
*
|
|
45
71
|
* Falls back to an empty array on failure — never throws.
|
|
46
72
|
*/
|
|
47
|
-
async discoverModels(providerId, apiKey, baseUrl) {
|
|
48
|
-
|
|
73
|
+
async discoverModels(providerId, apiKey, baseUrl, signal) {
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
return [];
|
|
76
|
+
const cacheKey = this.cacheKey(providerId, baseUrl);
|
|
49
77
|
const cached = this.discoveryCache.get(cacheKey);
|
|
50
78
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
51
79
|
return cached.models;
|
|
52
80
|
}
|
|
81
|
+
const pending = this.pendingDiscovery.get(cacheKey);
|
|
82
|
+
if (pending && !pending.signal?.aborted)
|
|
83
|
+
return pending.promise;
|
|
53
84
|
if (!this.providerRegistry)
|
|
54
85
|
return [];
|
|
55
86
|
const provider = this.providerRegistry.get(providerId);
|
|
56
87
|
if (!provider)
|
|
57
88
|
return [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (model.maxOutputTokens === DEFAULT_MAX_OUTPUT_TOKENS)
|
|
77
|
-
continue;
|
|
78
|
-
learnModelCeiling(providerId, model.id, model.maxOutputTokens, 'discovery');
|
|
89
|
+
const token = Symbol();
|
|
90
|
+
const work = (async () => {
|
|
91
|
+
try {
|
|
92
|
+
const models = await provider.listModels(apiKey, baseUrl, signal);
|
|
93
|
+
if (signal?.aborted || this.pendingDiscovery.get(cacheKey)?.token !== token)
|
|
94
|
+
return [];
|
|
95
|
+
// An output placeholder must not outrank the static ceiling table.
|
|
96
|
+
// Context provenance is resolved separately by getContextWindow().
|
|
97
|
+
for (const model of models) {
|
|
98
|
+
if (model.maxOutputTokens === DEFAULT_MAX_OUTPUT_TOKENS)
|
|
99
|
+
continue;
|
|
100
|
+
learnModelCeiling(providerId, model.id, model.maxOutputTokens, 'discovery');
|
|
101
|
+
}
|
|
102
|
+
this.discoveryCache.set(cacheKey, { models, timestamp: Date.now() });
|
|
103
|
+
return models;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
79
107
|
}
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
})();
|
|
109
|
+
this.pendingDiscovery.set(cacheKey, { promise: work, signal, token });
|
|
110
|
+
try {
|
|
111
|
+
return await work;
|
|
82
112
|
}
|
|
83
|
-
|
|
84
|
-
|
|
113
|
+
finally {
|
|
114
|
+
if (this.pendingDiscovery.get(cacheKey)?.promise === work)
|
|
115
|
+
this.pendingDiscovery.delete(cacheKey);
|
|
85
116
|
}
|
|
86
117
|
}
|
|
87
118
|
/** Invalidate the discovery cache for a provider (or all). */
|
|
88
119
|
clearCache(providerId) {
|
|
89
120
|
if (providerId) {
|
|
121
|
+
for (const key of this.pendingDiscovery.keys()) {
|
|
122
|
+
if (key.startsWith(`${providerId}:`))
|
|
123
|
+
this.pendingDiscovery.delete(key);
|
|
124
|
+
}
|
|
90
125
|
for (const key of this.discoveryCache.keys()) {
|
|
91
126
|
if (key.startsWith(`${providerId}:`)) {
|
|
92
127
|
this.discoveryCache.delete(key);
|
|
@@ -94,6 +129,7 @@ export class ModelRegistry {
|
|
|
94
129
|
}
|
|
95
130
|
}
|
|
96
131
|
else {
|
|
132
|
+
this.pendingDiscovery.clear();
|
|
97
133
|
this.discoveryCache.clear();
|
|
98
134
|
}
|
|
99
135
|
}
|
|
@@ -108,6 +144,7 @@ export class ModelRegistry {
|
|
|
108
144
|
name: modelId,
|
|
109
145
|
provider: providerId,
|
|
110
146
|
contextWindow: 128_000,
|
|
147
|
+
contextWindowSource: 'fallback',
|
|
111
148
|
// The PRODUCT DEFAULT, not a private guess. An unknown model that claims a
|
|
112
149
|
// ceiling nothing else agrees with is how one part of the system clamps
|
|
113
150
|
// output the rest of it never asked for.
|
|
@@ -168,7 +205,8 @@ const ANTHROPIC_MODELS = [
|
|
|
168
205
|
id: 'claude-opus-4-6',
|
|
169
206
|
name: 'Claude Opus 4.6',
|
|
170
207
|
provider: 'anthropic',
|
|
171
|
-
|
|
208
|
+
// https://platform.claude.com/docs/en/models/opus-4-6/overview
|
|
209
|
+
contextWindow: 1_000_000,
|
|
172
210
|
maxOutputTokens: 64_000,
|
|
173
211
|
supportsThinking: true,
|
|
174
212
|
supportsTools: true,
|
package/dist/llm/provider.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface LLMProvider {
|
|
|
71
71
|
* Implementations should apply a reasonable timeout (15 s) and return an
|
|
72
72
|
* empty array on failure rather than throwing.
|
|
73
73
|
*/
|
|
74
|
-
listModels(apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
74
|
+
listModels(apiKey: string, baseUrl?: string, signal?: AbortSignal): Promise<ModelInfo[]>;
|
|
75
75
|
}
|
|
76
76
|
export type LLMErrorType = 'auth_error' | 'rate_limit' | 'overloaded' | 'invalid_request' | 'context_overflow' | 'network_error' | 'timeout' | 'server_error' | 'unknown';
|
|
77
77
|
export declare class LLMError extends Error {
|
|
@@ -19,5 +19,5 @@ export declare class AnthropicProvider implements LLMProvider {
|
|
|
19
19
|
readonly defaultBaseUrl = "https://api.anthropic.com";
|
|
20
20
|
stream(request: LLMRequest): AsyncIterableIterator<StreamEvent>;
|
|
21
21
|
complete(request: LLMRequest): Promise<AssistantMessage>;
|
|
22
|
-
listModels(apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
22
|
+
listModels(apiKey: string, baseUrl?: string, signal?: AbortSignal): Promise<ModelInfo[]>;
|
|
23
23
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - Multimodal user messages (text + image)
|
|
12
12
|
* - Model list via `GET /v1/models`
|
|
13
13
|
*/
|
|
14
|
+
import { reportedContextWindow } from '../context-window.js';
|
|
15
|
+
import { createModelDiscoverySignal } from '../model-discovery.js';
|
|
14
16
|
import { THINKING_BUDGET } from '../types.js';
|
|
15
17
|
import { LLMError, classifyHttpError, wrapFetchError } from '../provider.js';
|
|
16
18
|
import { DEFAULT_MAX_OUTPUT_TOKENS, resolveOutputTokens, staticCeilingFor, } from '../output-limits.js';
|
|
@@ -282,30 +284,49 @@ export class AnthropicProvider {
|
|
|
282
284
|
// -----------------------------------------------------------------------
|
|
283
285
|
// listModels
|
|
284
286
|
// -----------------------------------------------------------------------
|
|
285
|
-
async listModels(apiKey, baseUrl) {
|
|
287
|
+
async listModels(apiKey, baseUrl, signal) {
|
|
286
288
|
const base = (baseUrl || this.defaultBaseUrl).replace(/\/+$/, '');
|
|
287
289
|
const url = `${base}/v1/models`;
|
|
290
|
+
const scope = createModelDiscoverySignal(signal);
|
|
288
291
|
try {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
292
|
+
const models = [];
|
|
293
|
+
const seen = new Set();
|
|
294
|
+
let cursor;
|
|
295
|
+
for (let page = 0; page < 20 && !scope.signal.aborted; page++) {
|
|
296
|
+
try {
|
|
297
|
+
const pageUrl = cursor === undefined ? url : `${url}?after_id=${encodeURIComponent(cursor)}`;
|
|
298
|
+
const res = await fetch(pageUrl, {
|
|
299
|
+
method: 'GET',
|
|
300
|
+
headers: { 'x-api-key': apiKey, 'anthropic-version': ANTHROPIC_VERSION },
|
|
301
|
+
signal: scope.signal,
|
|
302
|
+
});
|
|
303
|
+
if (!res.ok)
|
|
304
|
+
break;
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
if (!Array.isArray(data.data))
|
|
307
|
+
break;
|
|
308
|
+
models.push(...data.data);
|
|
309
|
+
const next = data.last_id;
|
|
310
|
+
if (data.has_more !== true || typeof next !== 'string' || next.length === 0 || seen.has(next))
|
|
311
|
+
break;
|
|
312
|
+
seen.add(next);
|
|
313
|
+
cursor = next;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
break; // Preserve metadata from earlier pages when continuation fails.
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (signal?.aborted)
|
|
298
320
|
return [];
|
|
299
|
-
const data = await res.json();
|
|
300
321
|
// The Anthropic models endpoint does NOT report an output ceiling, so this
|
|
301
322
|
// maps through the static table and deliberately does NOT call
|
|
302
323
|
// `learnModelCeiling`: asserting 64000 here is how `claude-3-5-haiku` ends
|
|
303
324
|
// up with a "discovered" ceiling that outranks its correct table entry.
|
|
304
|
-
return
|
|
325
|
+
return models.map((m) => ({
|
|
305
326
|
id: String(m.id || ''),
|
|
306
327
|
name: String(m.display_name || m.id || ''),
|
|
307
328
|
provider: this.id,
|
|
308
|
-
|
|
329
|
+
...reportedContextWindow(m),
|
|
309
330
|
maxOutputTokens: staticCeilingFor(this.id, String(m.id || '')) ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
310
331
|
supportsThinking: /claude-(sonnet-4|opus-4)/i.test(String(m.id)),
|
|
311
332
|
supportsTools: true,
|
|
@@ -316,6 +337,9 @@ export class AnthropicProvider {
|
|
|
316
337
|
catch {
|
|
317
338
|
return [];
|
|
318
339
|
}
|
|
340
|
+
finally {
|
|
341
|
+
scope.dispose();
|
|
342
|
+
}
|
|
319
343
|
}
|
|
320
344
|
}
|
|
321
345
|
// ---------------------------------------------------------------------------
|
|
@@ -18,5 +18,5 @@ export declare class GoogleProvider implements LLMProvider {
|
|
|
18
18
|
readonly defaultBaseUrl = "https://generativelanguage.googleapis.com";
|
|
19
19
|
stream(request: LLMRequest): AsyncIterableIterator<StreamEvent>;
|
|
20
20
|
complete(request: LLMRequest): Promise<AssistantMessage>;
|
|
21
|
-
listModels(apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
21
|
+
listModels(apiKey: string, baseUrl?: string, signal?: AbortSignal): Promise<ModelInfo[]>;
|
|
22
22
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - The API key is passed as a query parameter, not a header.
|
|
12
12
|
*/
|
|
13
13
|
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { reportedContextWindow } from '../context-window.js';
|
|
15
|
+
import { createModelDiscoverySignal } from '../model-discovery.js';
|
|
14
16
|
import { LLMError, classifyHttpError, wrapFetchError } from '../provider.js';
|
|
15
17
|
import { DEFAULT_MAX_OUTPUT_TOKENS, learnModelCeiling, resolveOutputTokens, } from '../output-limits.js';
|
|
16
18
|
import { sendWithOutputLimitRecovery } from '../output-limit-recovery.js';
|
|
@@ -199,18 +201,39 @@ export class GoogleProvider {
|
|
|
199
201
|
// -----------------------------------------------------------------------
|
|
200
202
|
// listModels
|
|
201
203
|
// -----------------------------------------------------------------------
|
|
202
|
-
async listModels(apiKey, baseUrl) {
|
|
204
|
+
async listModels(apiKey, baseUrl, signal) {
|
|
203
205
|
const base = (baseUrl || this.defaultBaseUrl).replace(/\/+$/, '');
|
|
204
206
|
const url = `${base}/v1beta/models?key=${encodeURIComponent(apiKey)}`;
|
|
207
|
+
const scope = createModelDiscoverySignal(signal);
|
|
205
208
|
try {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
const models = [];
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
let cursor;
|
|
212
|
+
for (let page = 0; page < 20 && !scope.signal.aborted; page++) {
|
|
213
|
+
try {
|
|
214
|
+
const pageUrl = cursor === undefined ? url : `${url}&pageToken=${encodeURIComponent(cursor)}`;
|
|
215
|
+
const res = await fetch(pageUrl, { method: 'GET', signal: scope.signal });
|
|
216
|
+
if (!res.ok)
|
|
217
|
+
break;
|
|
218
|
+
const data = await res.json();
|
|
219
|
+
if (!Array.isArray(data.models))
|
|
220
|
+
break;
|
|
221
|
+
models.push(...data.models);
|
|
222
|
+
const next = data.nextPageToken;
|
|
223
|
+
if (typeof next !== 'string' || next.length === 0 || seen.has(next))
|
|
224
|
+
break;
|
|
225
|
+
seen.add(next);
|
|
226
|
+
cursor = next;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
break; // Preserve metadata from earlier pages when continuation fails.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Explicit session cancellation must not publish partial results or learn ceilings.
|
|
233
|
+
// The internal timeout still permits useful metadata from completed pages.
|
|
234
|
+
if (signal?.aborted)
|
|
211
235
|
return [];
|
|
212
|
-
|
|
213
|
-
return (data.models || [])
|
|
236
|
+
return models
|
|
214
237
|
.filter((m) => {
|
|
215
238
|
const methods = (m.supportedGenerationMethods || []);
|
|
216
239
|
return methods.includes('generateContent') || methods.includes('streamGenerateContent');
|
|
@@ -228,7 +251,7 @@ export class GoogleProvider {
|
|
|
228
251
|
id,
|
|
229
252
|
name: String(m.displayName || id),
|
|
230
253
|
provider: this.id,
|
|
231
|
-
|
|
254
|
+
...reportedContextWindow(m),
|
|
232
255
|
maxOutputTokens: reported || DEFAULT_MAX_OUTPUT_TOKENS,
|
|
233
256
|
supportsThinking: false,
|
|
234
257
|
supportsTools: true,
|
|
@@ -240,6 +263,9 @@ export class GoogleProvider {
|
|
|
240
263
|
catch {
|
|
241
264
|
return [];
|
|
242
265
|
}
|
|
266
|
+
finally {
|
|
267
|
+
scope.dispose();
|
|
268
|
+
}
|
|
243
269
|
}
|
|
244
270
|
}
|
|
245
271
|
// ---------------------------------------------------------------------------
|
|
@@ -28,5 +28,5 @@ export declare class OpenAIProvider implements LLMProvider {
|
|
|
28
28
|
readonly defaultBaseUrl = "https://api.openai.com/v1";
|
|
29
29
|
stream(request: LLMRequest): AsyncIterableIterator<StreamEvent>;
|
|
30
30
|
complete(request: LLMRequest): Promise<AssistantMessage>;
|
|
31
|
-
listModels(apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
31
|
+
listModels(apiKey: string, baseUrl?: string, signal?: AbortSignal): Promise<ModelInfo[]>;
|
|
32
32
|
}
|
|
@@ -18,6 +18,8 @@ import { consumeStream } from '../stream-utils.js';
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
// Constants
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
|
+
import { reportedContextWindow } from '../context-window.js';
|
|
22
|
+
import { createModelDiscoverySignal } from '../model-discovery.js';
|
|
21
23
|
const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
22
24
|
export const OPENAI_TERMINAL_DRAIN_MS = 750;
|
|
23
25
|
/** Reasoning-family models reject `max_tokens` in favour of `max_completion_tokens`. */
|
|
@@ -111,20 +113,25 @@ export class OpenAIProvider {
|
|
|
111
113
|
// -----------------------------------------------------------------------
|
|
112
114
|
// listModels
|
|
113
115
|
// -----------------------------------------------------------------------
|
|
114
|
-
async listModels(apiKey, baseUrl) {
|
|
116
|
+
async listModels(apiKey, baseUrl, signal) {
|
|
115
117
|
const base = (baseUrl || this.defaultBaseUrl).replace(/\/+$/, '');
|
|
116
118
|
const url = `${base}/models`;
|
|
119
|
+
const scope = createModelDiscoverySignal(signal);
|
|
117
120
|
try {
|
|
121
|
+
if (scope.signal.aborted)
|
|
122
|
+
return [];
|
|
118
123
|
const res = await fetch(url, {
|
|
119
124
|
method: 'GET',
|
|
120
125
|
headers: {
|
|
121
126
|
'Authorization': `Bearer ${apiKey}`,
|
|
122
127
|
},
|
|
123
|
-
signal:
|
|
128
|
+
signal: scope.signal,
|
|
124
129
|
});
|
|
125
130
|
if (!res.ok)
|
|
126
131
|
return [];
|
|
127
132
|
const data = await res.json();
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
return [];
|
|
128
135
|
// The OpenAI models endpoint does NOT report an output ceiling, so this
|
|
129
136
|
// maps through the static table and deliberately does NOT call
|
|
130
137
|
// `learnModelCeiling` — claiming knowledge we do not have would outrank
|
|
@@ -135,7 +142,7 @@ export class OpenAIProvider {
|
|
|
135
142
|
id: String(m.id || ''),
|
|
136
143
|
name: String(m.id || ''),
|
|
137
144
|
provider: this.id,
|
|
138
|
-
|
|
145
|
+
...reportedContextWindow(m),
|
|
139
146
|
maxOutputTokens: staticCeilingFor(this.id, String(m.id || '')) ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
140
147
|
supportsThinking: false,
|
|
141
148
|
supportsTools: true,
|
|
@@ -146,6 +153,9 @@ export class OpenAIProvider {
|
|
|
146
153
|
catch {
|
|
147
154
|
return [];
|
|
148
155
|
}
|
|
156
|
+
finally {
|
|
157
|
+
scope.dispose();
|
|
158
|
+
}
|
|
149
159
|
}
|
|
150
160
|
}
|
|
151
161
|
/** Owns the request signal and the absolute, non-renewable terminal deadline. */
|
package/dist/llm/types.d.ts
CHANGED
|
@@ -160,6 +160,8 @@ export interface ModelInfo {
|
|
|
160
160
|
name: string;
|
|
161
161
|
provider: string;
|
|
162
162
|
contextWindow: number;
|
|
163
|
+
/** Provenance of the context limit; omitted by legacy/custom providers. */
|
|
164
|
+
contextWindowSource?: 'api' | 'catalog' | 'fallback';
|
|
163
165
|
maxOutputTokens: number;
|
|
164
166
|
supportsThinking: boolean;
|
|
165
167
|
supportsTools: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "AragonAgent core engine — LLM provider adapters, streaming, tool registry/execution, steering, idle watchdog, and an optional isolated-vm sandbox.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|