@shanepadgett/tau-agent 0.19.0 → 0.20.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.
@@ -1,11 +1,13 @@
1
1
  # Image Generation
2
2
 
3
- `image_gen` generates raster images and edits up to three local raster images with Grok Imagine. It uses `grok-imagine-image-quality` and saves results under `~/.local/share/tau-agent/images/` by default. Pass an explicit path with the expected image extension when the image should be saved in the current repository or another chosen location.
3
+ `image_gen` generates raster images and edits up to three local raster images with OpenAI GPT Image or xAI Grok Imagine. It uses `gpt-image-2` for OpenAI and `grok-imagine-image-quality` for xAI.
4
4
 
5
- Run `/login xai` and choose either a subscription or API-key login before invoking the tool.
5
+ By default, the tool follows the parent model: GPT models prefer OpenAI and Grok models prefer xAI. If that provider has no configured authentication, it tries the other provider. The model can pass `provider: "openai"` or `provider: "xai"` to override automatic selection.
6
+
7
+ Run `/login openai-codex` for GPT Image or `/login xai` for Grok Imagine before invoking the tool. OpenAI image generation uses the existing Codex subscription login; xAI supports its configured login methods.
6
8
 
7
9
  Run `/reload` after installing or changing the extension.
8
10
 
9
- The model invokes `image_gen` with a prompt. For edits, it also supplies one to three local PNG, JPEG, or WebP paths. Successful images up to 12 MiB are returned inline for inspection; larger results remain available at the saved path.
11
+ Results are saved under `~/.local/share/tau-agent/images/` by default. Pass an explicit path with the expected image extension when the image should be saved in the current repository or another chosen location. For edits, supply one to three local PNG, JPEG, or WebP paths. Successful images up to 12 MiB are returned inline for inspection; larger results remain available at the saved path.
10
12
 
11
- xAI controls model availability and subscription entitlements. A successful login does not guarantee that every account can use Grok Imagine.
13
+ The OpenAI path calls a private Codex backend, which may change without notice. xAI controls Grok Imagine availability and subscription entitlements. A successful login does not guarantee image access.
@@ -1,4 +1,10 @@
1
- import { XAI_API_BASE_URL, XAI_IMAGE_MODEL } from "./constants.ts";
1
+ import {
2
+ OPENAI_IMAGE_API_BASE_URL,
3
+ OPENAI_IMAGE_MODEL,
4
+ XAI_API_BASE_URL,
5
+ XAI_IMAGE_MODEL,
6
+ type ImageProvider,
7
+ } from "./constants.ts";
2
8
 
3
9
  const MAX_ERROR_BODY_BYTES = 8192;
4
10
  const MAX_ERROR_MESSAGE_LENGTH = 2000;
@@ -6,6 +12,11 @@ const REQUEST_TIMEOUT_MS = 60_000;
6
12
  const MAX_ATTEMPTS = 3;
7
13
  const RETRY_BASE_DELAY_MS = 500;
8
14
 
15
+ export interface CodexAuth {
16
+ token: string;
17
+ accountId: string;
18
+ }
19
+
9
20
  export interface EditImage {
10
21
  mimeType: "image/png" | "image/jpeg" | "image/webp";
11
22
  data: string;
@@ -28,6 +39,29 @@ function isRecord(value: unknown): value is Record<string, unknown> {
28
39
  return typeof value === "object" && value !== null && !Array.isArray(value);
29
40
  }
30
41
 
42
+ export function resolveCodexAuth(token: string): CodexAuth {
43
+ const invalidCredential = () =>
44
+ new Error("The OpenAI Codex credential does not contain a usable ChatGPT account ID. Run /login again.");
45
+ const segments = token.split(".");
46
+ if (segments.length !== 3 || !segments[1]) throw invalidCredential();
47
+ if (!/^[A-Za-z0-9_-]+$/.test(segments[1]) || segments[1].length % 4 === 1) throw invalidCredential();
48
+
49
+ let payload: unknown;
50
+ try {
51
+ const decoded = Buffer.from(segments[1], "base64url");
52
+ if (decoded.toString("base64url") !== segments[1]) throw invalidCredential();
53
+ payload = JSON.parse(decoded.toString("utf8"));
54
+ } catch {
55
+ throw invalidCredential();
56
+ }
57
+ if (!isRecord(payload)) throw invalidCredential();
58
+ const authClaim = payload["https://api.openai.com/auth"];
59
+ if (!isRecord(authClaim)) throw invalidCredential();
60
+ const accountId = authClaim.chatgpt_account_id;
61
+ if (typeof accountId !== "string" || !accountId.trim()) throw invalidCredential();
62
+ return { token, accountId: accountId.trim() };
63
+ }
64
+
31
65
  async function boundedError(response: HttpResponse): Promise<string> {
32
66
  if (!response.body) return "";
33
67
  const reader = response.body.getReader();
@@ -85,25 +119,61 @@ export function detectImageMimeType(bytes: Buffer): GeneratedImage["mimeType"] |
85
119
  return undefined;
86
120
  }
87
121
 
88
- function decodeImageResponse(value: unknown): GeneratedImage {
122
+ function decodeImageResponse(value: unknown, service: "OpenAI Codex" | "xAI", pngOnly: boolean): GeneratedImage {
89
123
  if (!isRecord(value) || !Array.isArray(value.data) || !isRecord(value.data[0])) {
90
- throw new Error("xAI returned an invalid image response");
124
+ throw new Error(`${service} returned an invalid image response`);
91
125
  }
92
126
  const encoded = value.data[0].b64_json;
93
- if (typeof encoded !== "string" || !encoded.trim()) throw new Error("xAI returned an invalid image response");
127
+ if (typeof encoded !== "string" || !encoded.trim()) throw new Error(`${service} returned an invalid image response`);
94
128
  const base64 = encoded.trim();
95
129
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64) || base64.length % 4 === 1) {
96
- throw new Error("xAI returned invalid base64 image data");
130
+ throw new Error(`${service} returned invalid base64 image data`);
97
131
  }
98
132
  const bytes = Buffer.from(base64, "base64");
99
133
  if (bytes.toString("base64").replace(/=+$/, "") !== base64.replace(/=+$/, "")) {
100
- throw new Error("xAI returned invalid base64 image data");
134
+ throw new Error(`${service} returned invalid base64 image data`);
101
135
  }
102
136
  const mimeType = detectImageMimeType(bytes);
103
- if (!mimeType) throw new Error("xAI returned unsupported image data");
137
+ if (!mimeType || (pngOnly && mimeType !== "image/png")) {
138
+ throw new Error(`${service} returned unsupported image data`);
139
+ }
104
140
  return { bytes, base64: bytes.toString("base64"), mimeType };
105
141
  }
106
142
 
143
+ async function requestOpenAIImage(
144
+ operation: "generation" | "edit",
145
+ body: Record<string, unknown>,
146
+ auth: CodexAuth,
147
+ signal?: AbortSignal,
148
+ ): Promise<GeneratedImage> {
149
+ const route = operation === "generation" ? "generations" : "edits";
150
+ const response = (await fetch(`${OPENAI_IMAGE_API_BASE_URL}/${route}`, {
151
+ method: "POST",
152
+ headers: {
153
+ Accept: "application/json",
154
+ Authorization: `Bearer ${auth.token}`,
155
+ "chatgpt-account-id": auth.accountId,
156
+ "Content-Type": "application/json",
157
+ originator: "pi",
158
+ },
159
+ body: JSON.stringify(body),
160
+ signal,
161
+ })) as HttpResponse;
162
+ if (!response.ok) {
163
+ const message = serverErrorMessage(await boundedError(response), auth.token);
164
+ throw new Error(
165
+ `OpenAI Codex image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`,
166
+ );
167
+ }
168
+ let value: unknown;
169
+ try {
170
+ value = await response.json();
171
+ } catch {
172
+ throw new Error("OpenAI Codex returned a non-JSON image response");
173
+ }
174
+ return decodeImageResponse(value, "OpenAI Codex", true);
175
+ }
176
+
107
177
  function retryable(status: number): boolean {
108
178
  return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
109
179
  }
@@ -124,7 +194,7 @@ async function delay(milliseconds: number, signal?: AbortSignal): Promise<void>
124
194
  });
125
195
  }
126
196
 
127
- async function requestImage(
197
+ async function requestXaiImage(
128
198
  operation: "generation" | "edit",
129
199
  body: Record<string, unknown>,
130
200
  token: string,
@@ -160,7 +230,7 @@ async function requestImage(
160
230
  } catch {
161
231
  throw new Error("xAI returned a non-JSON image response");
162
232
  }
163
- return decodeImageResponse(value);
233
+ return decodeImageResponse(value, "xAI", false);
164
234
  }
165
235
  const message = serverErrorMessage(await boundedError(response), token);
166
236
  if (!retryable(response.status) || attempt === MAX_ATTEMPTS) {
@@ -173,8 +243,21 @@ async function requestImage(
173
243
  throw new Error(`xAI image ${operation} failed`);
174
244
  }
175
245
 
176
- export function generateImage(prompt: string, token: string, signal?: AbortSignal): Promise<GeneratedImage> {
177
- return requestImage(
246
+ export function generateImage(
247
+ provider: ImageProvider,
248
+ prompt: string,
249
+ token: string,
250
+ signal?: AbortSignal,
251
+ ): Promise<GeneratedImage> {
252
+ if (provider === "openai") {
253
+ return requestOpenAIImage(
254
+ "generation",
255
+ { prompt, model: OPENAI_IMAGE_MODEL, background: "auto", quality: "auto", size: "auto" },
256
+ resolveCodexAuth(token),
257
+ signal,
258
+ );
259
+ }
260
+ return requestXaiImage(
178
261
  "generation",
179
262
  { model: XAI_IMAGE_MODEL, prompt, n: 1, resolution: "1k", response_format: "b64_json" },
180
263
  token,
@@ -183,13 +266,29 @@ export function generateImage(prompt: string, token: string, signal?: AbortSigna
183
266
  }
184
267
 
185
268
  export function editImage(
269
+ provider: ImageProvider,
186
270
  prompt: string,
187
271
  images: readonly EditImage[],
188
272
  token: string,
189
273
  signal?: AbortSignal,
190
274
  ): Promise<GeneratedImage> {
275
+ if (provider === "openai") {
276
+ return requestOpenAIImage(
277
+ "edit",
278
+ {
279
+ images: images.map((image) => ({ image_url: `data:${image.mimeType};base64,${image.data}` })),
280
+ prompt,
281
+ model: OPENAI_IMAGE_MODEL,
282
+ background: "auto",
283
+ quality: "auto",
284
+ size: "auto",
285
+ },
286
+ resolveCodexAuth(token),
287
+ signal,
288
+ );
289
+ }
191
290
  const references = images.map((image) => ({ url: `data:${image.mimeType};base64,${image.data}` }));
192
- return requestImage(
291
+ return requestXaiImage(
193
292
  "edit",
194
293
  {
195
294
  model: XAI_IMAGE_MODEL,
@@ -1,3 +1,8 @@
1
+ export const OPENAI_PROVIDER = "openai-codex";
2
+ export const OPENAI_IMAGE_MODEL = "gpt-image-2";
3
+ export const OPENAI_IMAGE_API_BASE_URL = "https://chatgpt.com/backend-api/codex/images";
1
4
  export const XAI_PROVIDER = "xai";
2
5
  export const XAI_IMAGE_MODEL = "grok-imagine-image-quality";
3
6
  export const XAI_API_BASE_URL = "https://api.x.ai/v1";
7
+
8
+ export type ImageProvider = "openai" | "xai";
@@ -5,7 +5,13 @@ import { homedir } from "node:os";
5
5
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
6
6
  import { type Static, Type } from "typebox";
7
7
  import { detectImageMimeType, editImage, generateImage, type EditImage, type GeneratedImage } from "./client.ts";
8
- import { XAI_IMAGE_MODEL, XAI_PROVIDER } from "./constants.ts";
8
+ import {
9
+ OPENAI_IMAGE_MODEL,
10
+ OPENAI_PROVIDER,
11
+ XAI_IMAGE_MODEL,
12
+ XAI_PROVIDER,
13
+ type ImageProvider,
14
+ } from "./constants.ts";
9
15
 
10
16
  const MAX_INPUT_BYTES = 50 * 1024 * 1024;
11
17
  const MAX_INLINE_BYTES = 12 * 1024 * 1024;
@@ -13,6 +19,12 @@ const MAX_INLINE_BYTES = 12 * 1024 * 1024;
13
19
  const imageGenSchema = Type.Object(
14
20
  {
15
21
  prompt: Type.String({ minLength: 1 }),
22
+ provider: Type.Optional(
23
+ Type.Union([Type.Literal("openai"), Type.Literal("xai")], {
24
+ description:
25
+ "Image provider override. Omit to follow the parent model, preferring OpenAI for GPT and xAI for Grok.",
26
+ }),
27
+ ),
16
28
  path: Type.Optional(
17
29
  Type.String({ description: "Explicit image destination path; defaults to Tau's external image store" }),
18
30
  ),
@@ -25,7 +37,8 @@ type ImageGenParams = Static<typeof imageGenSchema>;
25
37
 
26
38
  interface ImageGenDetails {
27
39
  path: string;
28
- model: typeof XAI_IMAGE_MODEL;
40
+ provider: ImageProvider;
41
+ model: typeof OPENAI_IMAGE_MODEL | typeof XAI_IMAGE_MODEL;
29
42
  operation: "generate" | "edit";
30
43
  }
31
44
 
@@ -41,7 +54,7 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
41
54
  name: "image_gen",
42
55
  label: "Image Generation",
43
56
  description:
44
- "Generate a requested raster image or AI-edit existing images with configured xAI authentication. Omit referenced_image_paths to generate; pass one to three local paths to edit or compose. Omit path to use Tau's external image store; pass path only when the user explicitly requests a repository file or other destination. Returns the image for inspection.",
57
+ "Generate a requested raster image or AI-edit existing images with OpenAI GPT Image or xAI Grok Imagine. Omit provider to follow the parent model; set it to openai or xai to override. Omit referenced_image_paths to generate; pass one to three local paths to edit or compose. Omit path to use Tau's external image store; pass path only when the user explicitly requests a repository file or other destination. Returns the image for inspection.",
45
58
  parameters: imageGenSchema,
46
59
  async execute(_toolCallId, params: ImageGenParams, signal, onUpdate, ctx) {
47
60
  signal?.throwIfAborted();
@@ -61,10 +74,38 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
61
74
  throw new Error("Image path must end in .jpg, .jpeg, .png, or .webp");
62
75
  }
63
76
 
64
- const token = await ctx.modelRegistry.getApiKeyForProvider(XAI_PROVIDER);
65
- if (!token) {
66
- throw new Error("xAI authentication is unavailable. Run /login xai and choose a login method.");
77
+ const parentUsesXai =
78
+ ctx.model?.provider.toLowerCase() === XAI_PROVIDER || ctx.model?.id.toLowerCase().includes("grok") === true;
79
+ const preferredProvider: ImageProvider = params.provider ?? (parentUsesXai ? "xai" : "openai");
80
+ const providers: readonly ImageProvider[] = params.provider
81
+ ? [params.provider]
82
+ : preferredProvider === "xai"
83
+ ? ["xai", "openai"]
84
+ : ["openai", "xai"];
85
+ let provider: ImageProvider | undefined;
86
+ let token: string | undefined;
87
+ for (const candidate of providers) {
88
+ const candidateToken = await ctx.modelRegistry.getApiKeyForProvider(
89
+ candidate === "openai" ? OPENAI_PROVIDER : XAI_PROVIDER,
90
+ );
91
+ if (candidateToken) {
92
+ provider = candidate;
93
+ token = candidateToken;
94
+ break;
95
+ }
96
+ }
97
+ if (!provider || !token) {
98
+ if (params.provider === "openai") {
99
+ throw new Error("OpenAI Codex authentication is unavailable. Run /login openai-codex.");
100
+ }
101
+ if (params.provider === "xai") {
102
+ throw new Error("xAI authentication is unavailable. Run /login xai and choose a login method.");
103
+ }
104
+ throw new Error(
105
+ "Image generation authentication is unavailable. Run /login for OpenAI Codex or xAI.",
106
+ );
67
107
  }
108
+ const model = provider === "openai" ? OPENAI_IMAGE_MODEL : XAI_IMAGE_MODEL;
68
109
  const images: EditImage[] = [];
69
110
  for (const path of params.referenced_image_paths ?? []) {
70
111
  const rawPath = path.startsWith("@") ? path.slice(1) : path;
@@ -88,16 +129,16 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
88
129
  type: "text",
89
130
  text:
90
131
  operation === "generate"
91
- ? `Generating image with ${XAI_IMAGE_MODEL}...`
92
- : `Editing image with ${XAI_IMAGE_MODEL}...`,
132
+ ? `Generating image with ${model}...`
133
+ : `Editing image with ${model}...`,
93
134
  },
94
135
  ],
95
136
  details: undefined,
96
137
  });
97
138
  const generated =
98
139
  operation === "generate"
99
- ? await generateImage(prompt, token, signal)
100
- : await editImage(prompt, images, token, signal);
140
+ ? await generateImage(provider, prompt, token, signal)
141
+ : await editImage(provider, prompt, images, token, signal);
101
142
  signal?.throwIfAborted();
102
143
  const generatedExtension = outputExtension(generated);
103
144
  if (requestedAbsolutePath) {
@@ -106,7 +147,9 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
106
147
  requestedExtension === generatedExtension ||
107
148
  (generatedExtension === ".jpg" && requestedExtension === ".jpeg");
108
149
  if (!matches)
109
- throw new Error(`xAI returned ${generated.mimeType}; destination must end in ${generatedExtension}`);
150
+ throw new Error(
151
+ `${provider === "openai" ? "OpenAI Codex" : "xAI"} returned ${generated.mimeType}; destination must end in ${generatedExtension}`,
152
+ );
110
153
  }
111
154
  const absolutePath =
112
155
  requestedAbsolutePath ??
@@ -129,13 +172,13 @@ export default function imageGenExtension(pi: ExtensionAPI): void {
129
172
  });
130
173
 
131
174
  const verb = operation === "generate" ? "Generated" : "Edited";
132
- const details: ImageGenDetails = { path: absolutePath, model: XAI_IMAGE_MODEL, operation };
175
+ const details: ImageGenDetails = { path: absolutePath, provider, model, operation };
133
176
  if (generated.bytes.length > MAX_INLINE_BYTES) {
134
177
  return {
135
178
  content: [
136
179
  {
137
180
  type: "text",
138
- text: `${verb} image saved to ${absolutePath}. The PNG exceeds the 12 MiB attachment limit, so it was not added to model context.`,
181
+ text: `${verb} image saved to ${absolutePath}. The image exceeds the 12 MiB attachment limit, so it was not added to model context.`,
139
182
  },
140
183
  ],
141
184
  details,
@@ -52,7 +52,7 @@ Adds `/ideas` to log rough ideas or open the ideas browser.
52
52
 
53
53
  ## image-gen
54
54
 
55
- Gives the agent a Grok Imagine generation and editing tool. Run `/login xai` and choose a subscription or API-key login before use. Generated images are saved for inspection.
55
+ Gives the agent an OpenAI GPT Image and xAI Grok Imagine generation and editing tool. It follows the parent model by default and can override the provider per request. Run `/login openai-codex` or `/login xai` before use. Generated images are saved for inspection.
56
56
 
57
57
  ## manage-sessions
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "README.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@shanepadgett/tau-tui": "0.19.0",
31
+ "@shanepadgett/tau-tui": "0.20.0",
32
32
  "@toon-format/toon": "2.3.0",
33
33
  "smol-toml": "1.7.0"
34
34
  },