pi-openai-toolkit 0.14.13 → 0.15.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,6 +1,7 @@
1
1
  import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { loadToolkitConfig } from "../config";
3
+ import { loadToolkitConfig, resolveToolkitConfig } from "../config";
4
+ import { notifyConfigIssues } from "../config/notifications";
4
5
  import { executeImageGeneration } from "./service";
5
6
  import { isImageGenerationEnabledForModel } from "./eligibility";
6
7
  import { renderImageGenerationResult } from "./render";
@@ -77,7 +78,7 @@ const GenerateImageParameters = Type.Object(
77
78
  ],
78
79
  {
79
80
  description:
80
- "Use null to accept the default image model, which is the first entry of imageGeneration.models; otherwise provide another model id exactly as it appears in that configured list.",
81
+ "Use null to accept the default image model, configured by imageGeneration.defaultModel; otherwise provide another model id exactly as it appears in that configured list.",
81
82
  },
82
83
  ),
83
84
  ),
@@ -115,14 +116,14 @@ export function registerImageGenerationExtension(
115
116
  label: "OpenAI Generate Image",
116
117
  description:
117
118
  "Generate a PNG image, or edit from one to five user-approved local reference images, through the current Responses-capable model and a configured hosted image_generation model. This is a paid provider operation.",
118
- promptSnippet: "Generate or edit PNG images through the current Responses-capable model and a configured imageGeneration.models entry.",
119
+ promptSnippet: "Generate or edit PNG images through the current Responses-capable model and a configured imageGeneration.allowedModels entry.",
119
120
  promptGuidelines: [
120
121
  "Use openai_generate_image when the user explicitly asks to create, draw, render, or edit a raster image and the active model speaks a Responses API.",
121
122
  "Do not call openai_generate_image speculatively: it consumes the user's provider or gateway image quota.",
122
123
  "Keep the image prompt faithful to the user's requested subject and constraints; do not invent unrequested style details.",
123
124
  "Set referenceImagePaths to null unless the user explicitly identified local files; never invent paths or placeholder strings, and remember upload requires user approval.",
124
125
  "Set outputPath to null unless the user explicitly asks for a destination; never invent a destination or placeholder string, and otherwise use the default Pi agent artifact.",
125
- "Set model to null unless the user named an image model configured in imageGeneration.models; never invent or guess a model id, and remember an unlisted model fails before the paid request.",
126
+ "Set model to null unless the user named an image model configured in imageGeneration.allowedModels; never invent or guess a model id, and remember an unlisted model fails before the paid request.",
126
127
  "Do not substitute Python, browser automation, shell scripts, or unrelated image tools for an eligible image request.",
127
128
  ],
128
129
  parameters: GenerateImageParameters,
@@ -151,20 +152,15 @@ export function registerImageGenerationExtension(
151
152
  renderResult: renderImageGenerationResult,
152
153
  });
153
154
 
154
- pi.on("session_start", (_event, ctx) => {
155
- const { config } = loadConfig();
156
- syncImageGenerationTool(pi, ctx.model, config.imageGeneration);
157
- });
158
-
159
- pi.on("model_select", (event) => {
160
- const { config } = loadConfig();
161
- syncImageGenerationTool(pi, event.model, config.imageGeneration);
162
- });
163
-
164
- pi.on("before_agent_start", (_event, ctx) => {
165
- const { config } = loadConfig();
166
- syncImageGenerationTool(pi, ctx.model, config.imageGeneration);
167
- });
155
+ const synchronize = (ctx: Parameters<typeof executeImageGeneration>[0]["ctx"], model = ctx.model) => {
156
+ const resolved = resolveToolkitConfig(loadConfig(), model);
157
+ notifyConfigIssues(ctx, resolved);
158
+ const valid = !resolved.invalidFeatures.some((feature) => feature === "imageGeneration" || feature === "compatibility");
159
+ syncImageGenerationTool(pi, model, { ...resolved.config.imageGeneration, enabled: valid && resolved.policy.imageGeneration.enabled });
160
+ };
161
+ pi.on("session_start", (_event, ctx) => synchronize(ctx));
162
+ pi.on("model_select", (event, ctx) => synchronize(ctx, event.model));
163
+ pi.on("before_agent_start", (_event, ctx) => synchronize(ctx));
168
164
  }
169
165
 
170
166
  export default function imageGenerationExtension(pi: ExtensionAPI): void {
@@ -154,8 +154,8 @@ function usableImageModels(models: readonly string[] | undefined): string[] {
154
154
 
155
155
  /**
156
156
  * Resolve the nested `image_generation` model for one call. An omitted request selects the
157
- * first configured model so configuration order expresses the default; an explicit request must
158
- * match a configured id exactly, and it fails here — before any paid dispatch.
157
+ * explicit default when supplied (v2), or the first configured model for legacy callers.
158
+ * An explicit request must match a configured id exactly before any paid dispatch.
159
159
  */
160
160
  function formatAvailableImageModels(models: readonly string[]): string {
161
161
  const visible = models.slice(0, MAX_DISPLAYED_IMAGE_MODELS_IN_ERROR);
@@ -166,15 +166,21 @@ function formatAvailableImageModels(models: readonly string[]): string {
166
166
  export function selectImageGenerationModel(args: {
167
167
  requestedModel?: string;
168
168
  configuredModels?: readonly string[];
169
+ defaultModel?: string;
169
170
  }): string {
170
171
  const models = usableImageModels(args.configuredModels);
172
+ if (args.defaultModel !== undefined && (!args.configuredModels?.length ||
173
+ !args.configuredModels.every((model) => typeof model === "string" && model.trim().length > 0 && model.trim().length <= MAX_IMAGE_MODEL_ID_CHARS) ||
174
+ !models.includes(args.defaultModel))) {
175
+ throw new ImageGenerationError("invalid-parameters", "The configured default image model must be in the nonempty allowedModels list.");
176
+ }
171
177
  const requested = typeof args.requestedModel === "string" ? args.requestedModel.trim() : "";
172
- if (!requested) return models[0]!;
178
+ if (!requested) return args.defaultModel ?? models[0]!;
173
179
  if (models.includes(requested)) return requested;
174
180
  throw new ImageGenerationError(
175
181
  "invalid-parameters",
176
182
  `Unknown image generation model "${requested.slice(0, MAX_IMAGE_MODEL_ID_CHARS)}". ` +
177
- `Configure it in imageGeneration.models first; available models: ${formatAvailableImageModels(models)}.`,
183
+ `Configure it in imageGeneration.allowedModels (legacy: imageGeneration.models) first; available models: ${formatAvailableImageModels(models)}.`,
178
184
  );
179
185
  }
180
186
 
@@ -1,5 +1,6 @@
1
1
  import { getAgentDir, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { loadToolkitConfig } from "../config";
2
+ import { assertConfigValid, loadToolkitConfig, resolveToolkitConfig } from "../config";
3
+ import { notifyConfigIssues } from "../config/notifications";
3
4
  import { resolveResponsesEnvironment } from "../runtime";
4
5
  import {
5
6
  copyImageToExplicitPath,
@@ -134,7 +135,10 @@ export function createImageGenerationExecutor(
134
135
  return async (args) => {
135
136
  const params = normalizeGenerateImageParams(args.params);
136
137
  throwIfAborted(args.signal);
137
- const { config } = deps.loadConfig();
138
+ const resolved = resolveToolkitConfig(deps.loadConfig(), args.ctx.model);
139
+ notifyConfigIssues(args.ctx, resolved);
140
+ assertConfigValid(resolved, "imageGeneration", "compatibility");
141
+ const { config } = resolved;
138
142
  if (!isImageGenerationEnabledForModel(args.ctx.model, config.imageGeneration)) {
139
143
  throw new ImageGenerationError(
140
144
  "unsupported-model",
@@ -144,12 +148,14 @@ export function createImageGenerationExecutor(
144
148
 
145
149
  const imageModel = selectImageGenerationModel({
146
150
  requestedModel: params.model,
147
- configuredModels: config.imageGeneration.models,
151
+ configuredModels: resolved.policy.imageGeneration.allowedModels,
152
+ defaultModel: resolved.policy.imageGeneration.defaultModel,
148
153
  });
149
154
 
150
155
  const runtimeResolution = await deps.resolveRuntime(args.ctx, {
151
156
  enabled: config.imageGeneration.enabled,
152
157
  responsesApis: IMAGE_GENERATION_CAPABLE_APIS,
158
+ ...(resolved.format === "v2" ? { codexGatewayModels: resolved.gatewayModelKeys } : {}),
153
159
  });
154
160
  if (!runtimeResolution.ok) {
155
161
  throw runtimeFailure(runtimeResolution.reason, runtimeResolution.errorMessage);
@@ -1,4 +1,5 @@
1
1
  import type { ResponsesCompatibleRequestPayload } from "./runtime";
2
+ import { LOCAL_WEB_SEARCH_TOOL_NAME, WEB_RUN_TOOL_NAME } from "./web-search/types";
2
3
 
3
4
  /**
4
5
  * Explicit allowlist of fields mirrored from the latest live Responses request.
@@ -44,7 +45,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
44
45
  function isWebSearchTool(value: unknown): boolean {
45
46
  if (!isRecord(value)) return false;
46
47
  if (value.type === "web_search" || value.type === "web_search_preview") return true;
47
- return value.type === "function" && (value.name === "web_search" || value.name === "web.run");
48
+ return value.type === "function" && (value.name === LOCAL_WEB_SEARCH_TOOL_NAME || value.name === WEB_RUN_TOOL_NAME);
48
49
  }
49
50
 
50
51
  function sameIdentity(cachedIdentity: RequestContextIdentity, current: RequestContextIdentity): boolean {