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
6 changes: 6 additions & 0 deletions .changeset/dsml-tool-call-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pymodel/pythinker-code": patch
"@pymodel/pythinker-desktop": patch
---

Run tool calls that some models (such as DeepSeek) write as DSML or <tool_call> text instead of showing them as plain text.
6 changes: 6 additions & 0 deletions .changeset/opencode-billing-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pymodel/pythinker-code": patch
"@pymodel/pythinker-desktop": patch
---

Report an insufficient-balance response from OpenAI-compatible providers as a billing error instead of an authentication error.
6 changes: 6 additions & 0 deletions .changeset/provider-env-api-key-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pymodel/pythinker-code": patch
"@pymodel/pythinker-desktop": patch
---

Accept a vendor API key or base URL set in a provider's env table (for example ANTHROPIC_API_KEY or ANTHROPIC_BASE_URL) instead of ignoring it.
2 changes: 1 addition & 1 deletion apps/pythinker-code/src/cli/sub/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export async function handleCatalogAdd(

// Always restore `[thinking]` from what was there before — including
// `undefined`. Persisting `enabled: false` when the user never set it would
// make `resolveThinkingEffort` (agent-core-v2/src/kosong/model/thinking.ts) treat
// make `resolveThinkingEffort` (agent-core-v2/src/llm-adapter/model/thinking.ts) treat
// it as an explicit "off" request and silently disable thinking, even for
// thinking-capable models.
config.thinking = previousThinking;
Expand Down
4 changes: 2 additions & 2 deletions apps/vis/server/src/lib/context-projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
selectCompactionUserMessages,
selectRecentUserMessages,
} from '@pymodel/agent-core-v2/agent/contextMemory/compactionHandoff';
import { estimateTokensForMessages } from '@pymodel/agent-core-v2/kosong/contract/tokens';
import { estimateTokensForMessages } from '@pymodel/agent-core-v2/llm-adapter/contract/tokens';
import { renderToolResultForModel } from '@pymodel/agent-core-v2/agent/contextMemory/toolResultRender';
import type {
ContentPart,
Expand Down Expand Up @@ -678,7 +678,7 @@ const MICRO_MIN_CONTENT_TOKENS = 100;

/** Replicates the engine's per-char token weighting exactly, over the same
* `text` + `think` parts its gate counts. The engine
* (`packages/agent-core-v2/src/kosong/contract/tokens.ts`) sums per-part
* (`packages/agent-core-v2/src/llm-adapter/contract/tokens.ts`) sums per-part
* estimates, each
* `estimateTokens(s) = Math.ceil(asciiCount / 4) + nonAsciiCount` (ASCII ~4
* chars/token, every non-ASCII/CJK code point a full token); other part types
Expand Down
2 changes: 1 addition & 1 deletion apps/vis/server/test/lib/context-projector.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// apps/vis/server/test/lib/context-projector.test.ts
import { describe, it, expect, afterEach } from 'vitest';
import { estimateTokensForMessages } from '@pymodel/agent-core-v2/kosong/contract/tokens';
import { estimateTokensForMessages } from '@pymodel/agent-core-v2/llm-adapter/contract/tokens';
import { buildCompactionContinuationText } from '@pymodel/agent-core-v2/agent/contextMemory/compactionHandoff';
import { buildSessionFixture } from '../fixtures/build';
import { projectContext } from '../../src/lib/context-projector';
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import { Error2 } from '#/_base/errors/errors';

import type { OAuthRef } from '#/kosong/provider/provider';
import type { OAuthRef } from '#/llm-adapter/provider/provider';

import { AuthErrors } from './errors';

Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/app/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import { type ILogger, ILogService } from '#/_base/log/log';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IConfigService } from '#/app/config/config';
import { LifecycleScope } from '#/app/scopes';
import { IModelService, type ModelRecord } from '#/kosong/model/model';
import { IModelService, type ModelRecord } from '#/llm-adapter/model/model';
import {
effectiveModelConfig,
nonEmpty,
providerNameFromFlatModel,
resolveModelAuthMaterial,
resolveModelForReady,
} from '#/kosong/model/modelAuth';
import { IProviderService, type OAuthRef } from '#/kosong/provider/provider';
} from '#/llm-adapter/model/model-auth';
import { IProviderService, type OAuthRef } from '#/llm-adapter/provider/provider';

import {
AuthModelNotResolvedError,
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/app/auth/authStatusService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { LifecycleScope } from '#/app/scopes';
import { IModelService } from '#/kosong/model/model';
import { resolveModelForReady } from '#/kosong/model/modelAuth';
import { IProviderService } from '#/kosong/provider/provider';
import { IModelService } from '#/llm-adapter/model/model';
import { resolveModelForReady } from '#/llm-adapter/model/model-auth';
import { IProviderService } from '#/llm-adapter/provider/provider';

import type { AuthSummary } from './authStatus';
import { IAuthStatusService } from './authStatus';
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/auth/configSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
transformPlainObject,
} from '#/app/config/toml';
import { type AssertExact, type Equal } from '#/_base/utils/typeEquality';
import type { OAuthRef } from '#/kosong/provider/provider';
import type { OAuthRef } from '#/llm-adapter/provider/provider';

export const SERVICES_SECTION = 'services';

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/config/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes';
import { CONFIG_INVALID_ERROR_CODE } from '#/kosong/contract/errors';
import { CONFIG_INVALID_ERROR_CODE } from '#/llm-adapter/contract/errors';

export const ConfigErrors = {
codes: {
Expand Down
8 changes: 4 additions & 4 deletions packages/agent-core-v2/src/app/kosongConfig/configSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import {
transformPlainObject,
} from '#/app/config/toml';
import { type AssertExact, type Equal } from '#/_base/utils/typeEquality';
import type { ModelOverride, ModelRecord, ModelsSection } from '#/kosong/model/model';
import type { ThinkingConfig } from '#/kosong/model/thinking';
import type { OAuthRef, ProviderConfig, ProvidersSection } from '#/kosong/provider/provider';
import { ProtocolSchema } from '#/kosong/protocol/protocol';
import type { ModelOverride, ModelRecord, ModelsSection } from '#/llm-adapter/model/model';
import type { ThinkingConfig } from '#/llm-adapter/model/thinking';
import type { OAuthRef, ProviderConfig, ProvidersSection } from '#/llm-adapter/provider/provider';
import { ProtocolSchema } from '#/llm-adapter/protocol/protocol';

export const PROVIDERS_SECTION = 'providers';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import { Error2 } from '#/_base/errors/errors';
import { IAgentIdentity } from '#/app/agentIdentity/agentIdentity';
import { IConfigService } from '#/app/config/config';
import { IEventService } from '#/app/event/event';
import { ModelCatalogErrors } from '#/kosong/model/errors';
import { modelRecordProviderId, type ModelRecord } from '#/kosong/model/model';
import { ModelCatalogErrors } from '#/llm-adapter/model/errors';
import { modelRecordProviderId, type ModelRecord } from '#/llm-adapter/model/model';
import {
IProviderService,
type ModelSource,
type ProviderConfig,
} from '#/kosong/provider/provider';
import { getProviderDefinition } from '#/kosong/provider/providerDefinition';
} from '#/llm-adapter/provider/provider';
import { getProviderDefinition } from '#/llm-adapter/provider/provider-definition';

import {
DEFAULT_MODEL_SECTION,
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/app/kosongConfig/envOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Error2 } from '#/_base/errors/errors';

import type { ConfigEffectiveOverlay } from '#/app/config/config';
import { registerConfigOverlay } from '#/app/config/configOverlayContributions';
import { CONFIG_INVALID_ERROR_CODE } from '#/kosong/contract/errors';
import { resolveProviderEndpoint } from '#/kosong/provider/providerDefinition';
import { CONFIG_INVALID_ERROR_CODE } from '#/llm-adapter/contract/errors';
import { resolveProviderEndpoint } from '#/llm-adapter/provider/provider-definition';
import {
PYTHINKER_BASE_URL_ENV,
PYTHINKER_DEFAULT_BASE_URL,
} from '#/kosong/provider/providers/pythinker/pythinker.contrib';
} from '#human/llm-pythinker/trait';

import { ENV_MODEL_PROVIDER_KEY } from './configSection';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { IAgentIdentity } from '#/app/agentIdentity/agentIdentity';
import { IBootstrapService } from '#/app/bootstrap/bootstrap';
import { IHostRequestHeaders } from '#/kosong/model/hostRequestHeaders';
import { IHostRequestHeaders } from '#/llm-adapter/model/host-request-headers';

export class HostRequestHeadersAdapter implements IHostRequestHeaders {
readonly headers: Readonly<Record<string, string>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { retryBackoffDelays, sleepForRetry } from '#/_base/utils/retry';
import { type ConfigSectionChangedEvent, IConfigService } from '#/app/config/config';
import { describeUnknownError } from '#/app/config/configPure';
import { deepEqual } from '#/app/config/sectionDiff';
import { IModelService, type ModelsSection } from '#/kosong/model/model';
import { IProviderService, type ProvidersSection } from '#/kosong/provider/provider';
import { IModelService, type ModelsSection } from '#/llm-adapter/model/model';
import { IProviderService, type ProvidersSection } from '#/llm-adapter/provider/provider';

import { IKosongConfigService } from './kosongConfig';
import {
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-core-v2/src/app/kosongConfig/modelsDev.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ModelCapability } from '#/kosong/contract/capability';
import type { ProviderType } from '#/kosong/provider/provider';
import type { ModelCapability } from '#/llm-adapter/contract/capability';
import type { ProviderType } from '#/llm-adapter/provider/provider';

import { wireHasProtocolThinkingDisable } from '#/kosong/model/thinking';
import { wireHasProtocolThinkingDisable } from '#/llm-adapter/model/thinking';

export interface ModelsDevModelEntry {
readonly id?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
import type { ProviderCatalogItem } from '#/kosong/model/catalog';
import type { ProviderCatalogItem } from '#/llm-adapter/model/catalog';

export interface ModelsDevModelItem {
readonly id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { Error2 } from '#/_base/errors/errors';
import { IAgentIdentity } from '#/app/agentIdentity/agentIdentity';
import { IConfigService } from '#/app/config/config';
import { IModelCatalog } from '#/kosong/model/catalog';
import { IModelService, modelRecordProviderId, type ModelsSection } from '#/kosong/model/model';
import { type ProviderConfig, type ProvidersSection } from '#/kosong/provider/provider';
import { IModelCatalog } from '#/llm-adapter/model/catalog';
import { IModelService, modelRecordProviderId, type ModelsSection } from '#/llm-adapter/model/model';
import { type ProviderConfig, type ProvidersSection } from '#/llm-adapter/provider/provider';
import { modelsDevProviderModels, resolveModelsDevImport } from './modelsDev';

import { DEFAULT_MODEL_SECTION, MODELS_SECTION, PROVIDERS_SECTION } from './configSection';
Expand Down Expand Up @@ -176,7 +176,6 @@ export class ModelsDevImportService implements IModelsDevImportService {
await this.seedDefaultModelWhenUnset(config, `${targetId}/${firstModel.id}`);
}

await this.models.settled;
const imported = await this.modelCatalog.getProvider(targetId);
return { provider: imported, modelsImported: models.length };
}
Expand Down Expand Up @@ -258,7 +257,6 @@ export class ModelsDevImportService implements IModelsDevImportService {
}
}

await this.models.settled;
const imported = [];
for (const entry of Object.values(entries)) {
imported.push(await this.modelCatalog.getProvider(entry.id));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CoreErrors } from '#/_base/errors/codes';
import { BugIndicatingError, Error2 } from '#/_base/errors/errors';
import type { ModelCapability } from '#/kosong/contract/capability';
import type { ModelRecord } from '#/kosong/model/model';
import type { ModelCapability } from '#/llm-adapter/contract/capability';
import type { ModelRecord } from '#/llm-adapter/model/model';

import { BUILT_IN_MODELS_DEV_JSON } from './builtInModelsDev';
import { ModelsDevImportErrors } from './errors';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { Error2 } from '#/_base/errors/errors';

import { IOAuthTokenService } from '#/app/auth/auth';
import { AuthErrors } from '#/app/auth/errors';
import { nonEmpty } from '#/kosong/model/modelAuth';
import { IModelOAuthTokens } from '#/kosong/model/modelOAuth';
import type { OAuthRef } from '#/kosong/provider/provider';
import { nonEmpty } from '#/llm-adapter/model/model-auth';
import { IModelOAuthTokens } from '#/llm-adapter/model/model-oauth';
import type { OAuthRef } from '#/llm-adapter/provider/provider';

export class ModelOAuthTokenAdapter implements IModelOAuthTokens {
declare readonly _serviceBrand: undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core-v2/src/app/plugin/pluginService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ITelemetryService } from '#/app/telemetry/telemetry';
import { ISkillDiscovery } from '#/features/skill/catalog/skillDiscovery';
import type { SkillRoot } from '#/features/skill/catalog/types';
import { BugIndicatingError, Error2, PluginErrors } from '#/errors';
import { IProviderService } from '#/kosong/provider/provider';
import { IProviderService } from '#/llm-adapter/provider/provider';
import type { HookDef } from '#/features/externalHooks/internal/types';
import type { McpServerConfig } from '#/mcpCore/config-schema';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { jitteredNextCronRunMs, oneShotJitteredNextCronRunMs } from '#/features/
import type { CronDeletedEvent, CronScheduledEvent } from '#/app/telemetry/events';
import { ITelemetryService } from '#/app/telemetry/telemetry';
import { BugIndicatingError } from '#/errors';
import type { ContentPart } from '#/kosong/contract/message';
import type { ContentPart } from '#/llm-adapter/contract/message';
import { IAgentReminderService } from '#/features/reminder/reminderService';
import { MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
import { Forked } from '#/session/agentLifecycle/forked';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { StreamedMessagePart, ToolCall } from '#/kosong/contract/message';
import type { StreamedMessagePart, ToolCall } from '#/llm/message';

const MARK = String.raw`\s*[||]?\s*(?:DSML\s*[||]?)?\s*`;
const CONTAINER_OPEN_RE = new RegExp(String.raw`<${MARK}tool_calls\s*>`, 'yi');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,21 @@ export function isContextOverflowErrorCode(code: string | null | undefined): boo
return code === 'context_length_exceeded';
}

const BILLING_REJECTION_PHRASES = [
'insufficient balance',
'insufficient credit',
'credits exhausted',
'please recharge',
];

function isOpenAIInsufficientQuotaError(error: RawOpenAISDKAPIError): boolean {
if (error.status !== 429) return false;
if (![401, 402, 403, 429].includes(error.status ?? 0)) return false;
if (typeof error.code === 'string' && isOpenAIInsufficientQuotaCode(error.code)) return true;
if (typeof error.type === 'string' && isOpenAIInsufficientQuotaCode(error.type)) return true;
return error.message.toLowerCase().includes('insufficient_quota');
const message = error.message.toLowerCase();
if (message.includes('insufficient_quota')) return true;
if (error.status === 429) return false;
return BILLING_REJECTION_PHRASES.some((phrase) => message.includes(phrase));
}

export function convertOpenAIError(
Expand Down Expand Up @@ -367,7 +377,7 @@ export function convertOpenAIError(
return {
kind: 'quota_exhausted',
message: sanitizeStatusErrorMessage(error.message),
statusCode: 429,
statusCode: error.status,
requestId,
retryAfterMs,
headers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import OpenAI from 'openai';
import { assign, shake } from 'radashi';

import { headersToRecord } from '#/llm/errors';
import type { FinishInfo } from '#/llm/finish-reason';
import type { StreamedMessagePart, ToolCall } from '#/llm/message';
import { modelKey, type LlmModel } from '#/llm/model';
import { toLlmSyntaxErrorMessage } from '#/llm/syntax-errors';
import type { ProtocolBase, ProtocolRequesterOptions, TraitContext } from '#/llm/protocol/base';
Expand Down Expand Up @@ -43,6 +45,7 @@ import {
type OpenAIRequestParams,
} from './format';
import { DEFAULT_REASONING_KEY, ReasoningKeyDialect } from './reasoning-key';
import { DsmlStreamParser } from './dsml-tool-parser';

const OPENAI_CHAT_TOOL_CALL_ID_POLICY: ToolCallIdPolicy = {
normalize: (id) => sanitizeToolCallId(id, 64),
Expand Down Expand Up @@ -173,12 +176,34 @@ async function executeOpenAIRequest(
},
});
let messageId: string | undefined;
const dsml = new DsmlStreamParser();
const recoveredToolCalls: ToolCall[] = [];
let nativeToolCallsSeen = false;
let finish: FinishInfo | undefined;
const emitPart = (part: StreamedMessagePart): void => {
onEvent?.({ type: 'llm.streaming.part', part });
};
const emitTextParts = (parts: readonly StreamedMessagePart[]): void => {
for (const part of parts) {
if (part.type === 'function') recoveredToolCalls.push(part);
else emitPart(part);
}
};
for await (const chunk of stream) {
reasoning.observe(chunk.choices?.[0]?.delta);
let failed = false;
parse(chunk, {
onDelta: (part) => onEvent?.({ type: 'llm.streaming.part', part }),
onFinish: (finish) => onEvent?.({ type: 'llm.streaming.finish', finish }),
onDelta: (part) => {
if (part.type === 'text') {
emitTextParts(dsml.feed(part.text));
return;
}
if (part.type === 'function' || part.type === 'tool_call_part') nativeToolCallsSeen = true;
emitPart(part);
},
onFinish: (next) => {
finish = next;
},
onMessageId: (id) => {
if (id === messageId) return;
messageId = id;
Expand All @@ -194,6 +219,20 @@ async function executeOpenAIRequest(
return;
}
}
emitTextParts(dsml.flush());
const recovered = !nativeToolCallsSeen && recoveredToolCalls.length > 0;
if (recovered) {
for (const toolCall of recoveredToolCalls) emitPart(toolCall);
}
if (finish !== undefined) {
onEvent?.({
type: 'llm.streaming.finish',
finish:
recovered && (finish.finishReason === 'completed' || finish.finishReason === null)
? { ...finish, finishReason: 'tool_calls' }
: finish,
});
}
onEvent?.({ type: 'llm.done' });
}

Expand Down
Loading
Loading