praxis-agent 0.53.1 → 0.54.0

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/README.md CHANGED
@@ -13,7 +13,8 @@ Praxis is a local-first, single-user general agent for the command line.
13
13
 
14
14
  It provides an interactive or headless agent loop, local tools, permissions,
15
15
  sessions, skills, hooks, MCP, plugins, background agents, and provider-neutral
16
- Anthropic/OpenAI-compatible model access. Praxis deliberately excludes
16
+ Anthropic, OpenAI-compatible Chat Completions, and OpenAI Responses model
17
+ access. Praxis deliberately excludes
17
18
  accounts, organizations, billing, managed enterprise policy, remote control,
18
19
  IDE surfaces, and telemetry control planes.
19
20
 
@@ -27,9 +28,9 @@ sessions, configuration, or compatibility directories.
27
28
  - macOS or Linux
28
29
  - Node.js 24 or newer
29
30
  - [`ripgrep`](https://github.com/BurntSushi/ripgrep) (`rg`) for the Grep tool
30
- - an API key and model ID for an Anthropic or OpenAI-compatible provider (the
31
- stable setup), or the explicitly enabled experimental ChatGPT-backed Codex
32
- subscription integration
31
+ - an API key and model ID for an Anthropic, OpenAI-compatible, or OpenAI
32
+ Responses provider (the stable setup), or the explicitly enabled experimental
33
+ ChatGPT-backed Codex subscription integration
33
34
 
34
35
  Praxis does not use Claude subscription authentication. Claude-shaped message,
35
36
  tool, and CLI protocol forms remain supported where they are part of the
@@ -63,6 +64,20 @@ cd /path/to/project
63
64
  praxis
64
65
  ```
65
66
 
67
+ To use OpenAI's Responses API with an explicit API-key provider:
68
+
69
+ ```sh
70
+ export PRAXIS_PROVIDER="openai-responses"
71
+ export OPENAI_API_KEY="your-api-key"
72
+ export PRAXIS_MODEL="your-responses-model-id"
73
+
74
+ cd /path/to/project
75
+ praxis
76
+ ```
77
+
78
+ The `openai` provider remains OpenAI-compatible Chat Completions. Provider
79
+ protocols are selected explicitly; model IDs never switch protocols implicitly.
80
+
66
81
  Praxis also has an experimental `openai-codex` provider for ChatGPT-backed
67
82
  Codex subscriptions. It is separate from OpenAI API-key access, requires
68
83
  `experimental.codexSubscription: true`, and stores OAuth credentials in the
@@ -238,7 +253,11 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
238
253
  recovery for malformed streamed tool arguments without tool execution or
239
254
  lost resumability, one default-on bounded Anthropic non-streaming replay for
240
255
  eligible stream/idle failures without exposing failed-attempt output, and
241
- token-only/no-API-dollar accounting for subscription runs.
256
+ token-only/no-API-dollar accounting for subscription runs. For each main user
257
+ turn, failed provider attempts stay buffered; the first successful route,
258
+ whether primary or fallback, is sticky and sealed through that turn's tool
259
+ continuations; incompatible fallback routes fail closed, and each new main
260
+ user turn starts from primary.
242
261
  - **Transactional self-update** — `praxis update` verifies the package before
243
262
  installing it, rejects concurrent updates, and can roll back after an
244
263
  interruption or crash.
@@ -70,6 +70,8 @@ export interface ClaudeSessionServiceOptions {
70
70
  enableWorkflows?: boolean;
71
71
  providerForModel?: (model: string) => ModelProvider;
72
72
  providerForMainModel?: (model: string) => ModelProvider;
73
+ /** Creates one fresh main-turn provider per outer user turn; never used for auxiliary model calls. */
74
+ providerForTurn?: (model?: string) => ModelProvider;
73
75
  /** Creates a provider adapter dedicated to Session memory requests so
74
76
  * adapter-local cache and retry state are not shared with the foreground. */
75
77
  sessionMemoryProviderFactory?: () => ModelProvider;
@@ -745,7 +745,16 @@ export class ClaudeSessionService {
745
745
  ? (options.extensions?.agent(options.agent) ?? null)
746
746
  : null;
747
747
  if (configuredAgent && options.provider) {
748
- this.activeProvider = this.providerForAgent(configuredAgent);
748
+ const selectedModel = !options.explicitModel &&
749
+ configuredAgent.model &&
750
+ configuredAgent.model !== 'inherit'
751
+ ? configuredAgent.model
752
+ : undefined;
753
+ const selectProvider = options.providerForMainModel ?? options.providerForModel;
754
+ this.activeProvider =
755
+ selectedModel !== undefined && selectProvider
756
+ ? selectProvider(selectedModel)
757
+ : this.options.provider;
749
758
  }
750
759
  }
751
760
  nextScheduledPrompt(signal) {
@@ -5043,6 +5052,12 @@ export class ClaudeSessionService {
5043
5052
  providerForAgent(agent) {
5044
5053
  const inherited = this.provider();
5045
5054
  const selectProvider = this.options.providerForMainModel ?? this.options.providerForModel;
5055
+ const effectiveModel = !this.options.explicitModel && agent?.model && agent.model !== 'inherit'
5056
+ ? agent.model
5057
+ : inherited.model;
5058
+ if (this.options.providerForTurn) {
5059
+ return this.options.providerForTurn(effectiveModel);
5060
+ }
5046
5061
  if (this.options.explicitModel ||
5047
5062
  !agent?.model ||
5048
5063
  agent.model === 'inherit' ||
@@ -1044,6 +1044,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1044
1044
  let provider;
1045
1045
  let providerForModel;
1046
1046
  let providerForMainModel;
1047
+ let providerForTurn;
1047
1048
  let providerBillingMode;
1048
1049
  const context = parseContextEnvironment(runtimeEnvironment);
1049
1050
  const apiKey = runtimeEnvironment.PRAXIS_API_KEY;
@@ -1164,17 +1165,25 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1164
1165
  providerBillingMode = registry.target.billingMode;
1165
1166
  providerForModel = (selectedModel) => registry.create(selectedModel);
1166
1167
  const createProvider = providerForModel;
1167
- providerForMainModel = (primaryModel) => {
1168
+ const createProviderStack = (primaryModel, routeScope) => {
1168
1169
  const models = [primaryModel, ...(cli.fallbackModels ?? [])].filter((candidate, index, all) => all.indexOf(candidate) === index);
1169
1170
  const providers = models.map((candidate) => createProvider(candidate));
1170
1171
  const selected = providers[0];
1171
1172
  if (!selected)
1172
1173
  throw new Error('A primary model is required');
1173
1174
  return providers.length > 1
1174
- ? new FallbackModelProvider({ providers })
1175
+ ? new FallbackModelProvider({ providers, routeScope })
1175
1176
  : selected;
1176
1177
  };
1177
- provider = providerForMainModel(model);
1178
+ providerForMainModel = (primaryModel) => createProviderStack(primaryModel, 'completion');
1179
+ const defaultProvider = providerForMainModel(model);
1180
+ provider = defaultProvider;
1181
+ providerForTurn = (turnModel) => {
1182
+ const selectedModel = turnModel ?? model;
1183
+ return selectedModel === undefined
1184
+ ? defaultProvider
1185
+ : createProviderStack(selectedModel, 'turn');
1186
+ };
1178
1187
  }
1179
1188
  catch (error) {
1180
1189
  const optionalProviderError = error instanceof ProviderAuthenticationError ||
@@ -1918,6 +1927,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1918
1927
  provider: hostedToolProvider,
1919
1928
  ...(providerForModel ? { providerForModel } : {}),
1920
1929
  ...(providerForMainModel ? { providerForMainModel } : {}),
1930
+ ...(providerForTurn ? { providerForTurn } : {}),
1921
1931
  tools: filteredTools,
1922
1932
  toolCapabilityEnvironment: runtimeEnvironment,
1923
1933
  ...(!experimentalNativeTranscriptWrites ? { mcp: mcpTools } : {}),
@@ -14,19 +14,13 @@ export interface CodexSubscriptionProviderOptions {
14
14
  maxErrorBodyBytes?: number;
15
15
  thinking?: ModelThinkingConfig;
16
16
  }
17
- export declare function serializeCodexRequest(request: ModelRequest, model: string, configuredThinking?: ModelThinkingConfig): Record<string, unknown>;
18
- export declare function parseCodexSseFrame(data: string): ModelStreamEvent[];
19
17
  export declare class CodexSubscriptionProvider implements ModelProvider {
20
18
  private readonly options;
21
19
  readonly model: string;
22
20
  readonly capabilities: ModelProvider['capabilities'];
23
21
  private readonly fetchImplementation;
24
- private readonly maxStreamBufferBytes;
25
- private readonly maxToolArgumentsBytes;
26
- private readonly maxToolCallsPerResponse;
27
- private readonly maxToolMetadataBytes;
28
- private readonly maxReasoningBytes;
29
22
  private readonly maxErrorBodyBytes;
23
+ private readonly responsesCodec;
30
24
  constructor(options: CodexSubscriptionProviderOptions);
31
25
  complete(request: ModelRequest): AsyncIterable<ModelStreamEvent>;
32
26
  }