oh-my-promptfoo 1.22.1 → 1.22.2-next.124

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
@@ -135,6 +135,7 @@ providers:
135
135
  config:
136
136
  model: gpt-5.6-sol # required, exactly as Copilot lists it
137
137
  reasoning_effort: medium # optional: low, medium or high
138
+ max_tokens: 32000 # optional; defaults to the model's output limit
138
139
  defaultTest:
139
140
  options:
140
141
  provider:
@@ -145,7 +146,7 @@ defaultTest:
145
146
  response_format: { type: json_object } # optional; the only accepted format
146
147
  ```
147
148
 
148
- Those three keys are the whole config. The endpoint and credentials always come
149
+ Those four keys are the whole config. The endpoint and credentials always come
149
150
  from Copilot: a private Copilot SDK process signs in with `GH_TOKEN` (never your
150
151
  stored logins or `OPENAI_*`, `ANTHROPIC_*` or `AZURE_*` settings), asks Copilot for
151
152
  the model's endpoint and short-lived credentials, and refreshes them before they
@@ -169,6 +170,21 @@ API in its own shape:
169
170
  - `response_format: { type: json_object }` works on Responses and Chat
170
171
  Completions. The Anthropic Messages API has no JSON-object mode, so a Claude
171
172
  model with `response_format` fails and names the model.
173
+ - `max_tokens` caps each reply's output tokens: the Responses
174
+ `max_output_tokens`, the Chat Completions `max_tokens` (or
175
+ `max_completion_tokens` for models Promptfoo treats as reasoning models) and
176
+ the Messages `max_tokens`. Without it, the cap is the model's
177
+ `capabilities.limits.max_output_tokens` from Copilot's model list, so long
178
+ answers are not cut off at Promptfoo's default of 1024 (2048 for Claude
179
+ thinking models). When the list reports no limit, Promptfoo's default
180
+ applies. The cap in force is in `metadata.copilotMaxOutputTokens`.
181
+ `OPENAI_MAX_TOKENS`, `OPENAI_MAX_COMPLETION_TOKENS` and
182
+ `ANTHROPIC_MAX_TOKENS` never override it.
183
+ - No request carries a `temperature`, so each model uses its own default.
184
+ Promptfoo's default of 0 and `OPENAI_TEMPERATURE` or `ANTHROPIC_TEMPERATURE`
185
+ are not sent; some models, such as Claude Opus 4.7 and later, reject them.
186
+ - Messages requests stream, because the Anthropic SDK refuses a
187
+ non-streaming request with a large `max_tokens`.
172
188
  - Claude replies contain only the answer, not its thinking.
173
189
 
174
190
  Every instance in a run shares that one SDK process. The provider id is
@@ -5659,6 +5659,7 @@ const CONFIG_KEYS = {
5659
5659
  model: true,
5660
5660
  reasoning_effort: true,
5661
5661
  response_format: true,
5662
+ max_tokens: true,
5662
5663
  basePath: true
5663
5664
  };
5664
5665
  /** Validate authored config; anything that could redirect endpoint or authentication is rejected. */
@@ -5667,17 +5668,30 @@ function copilotModelConfig(input) {
5667
5668
  const config = input;
5668
5669
  const unsupported = Object.keys(config).filter((key) => !Object.hasOwn(CONFIG_KEYS, key));
5669
5670
  if (unsupported.length) throw new Error(`CopilotModel does not accept config.${unsupported.join(", config.")}; the endpoint and credentials come from Copilot discovery.`);
5670
- const { model, reasoning_effort, response_format } = config;
5671
+ const { model, reasoning_effort, response_format, max_tokens } = config;
5671
5672
  if (typeof model !== "string" || !model.trim() || model !== model.trim()) throw new Error("CopilotModel requires an explicit config.model.");
5672
5673
  if (reasoning_effort !== void 0 && !EFFORTS.includes(reasoning_effort)) throw new Error(`CopilotModel config.reasoning_effort must be one of: ${EFFORTS.join(", ")}.`);
5673
5674
  if (response_format !== void 0 && (typeof response_format !== "object" || response_format === null || Object.keys(response_format).join() !== "type" || !("type" in response_format) || response_format.type !== "json_object")) throw new Error("CopilotModel config.response_format must be { type: \"json_object\" }.");
5675
+ if (max_tokens !== void 0 && !positiveInteger(max_tokens)) throw new Error("CopilotModel config.max_tokens must be a positive integer.");
5674
5676
  return Object.freeze({
5675
5677
  model,
5676
5678
  ...reasoning_effort === void 0 ? {} : { reasoning_effort },
5677
- ...response_format === void 0 ? {} : { response_format: Object.freeze({ type: "json_object" }) }
5679
+ ...response_format === void 0 ? {} : { response_format: Object.freeze({ type: "json_object" }) },
5680
+ ...max_tokens === void 0 ? {} : { max_tokens }
5678
5681
  });
5679
5682
  }
5680
5683
  const describe = (value) => typeof value === "string" ? JSON.stringify(value) : "none";
5684
+ const positiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
5685
+ /**
5686
+ * The model's maximum output tokens from Copilot's model list
5687
+ * (`capabilities.limits.max_output_tokens`), when reported.
5688
+ */
5689
+ function modelOutputLimit(models, model) {
5690
+ if (!Array.isArray(models)) return void 0;
5691
+ const limits = models.find((candidate) => candidate?.id === model)?.capabilities?.limits;
5692
+ const limit = limits && typeof limits === "object" ? limits.max_output_tokens : void 0;
5693
+ return positiveInteger(limit) ? limit : void 0;
5694
+ }
5681
5695
  /** The Copilot API a discovery snapshot describes; anything but the three HTTP APIs is rejected. */
5682
5696
  function endpointOf(snapshot, model) {
5683
5697
  const { type, wireApi, transport } = snapshot ?? {};
@@ -5751,8 +5765,15 @@ function anthropicReasoning(models, model, effort) {
5751
5765
  effort
5752
5766
  };
5753
5767
  }
5754
- /** The Promptfoo provider id and options that send `config` to the discovered endpoint. */
5755
- function nativeProvider(config, snapshot, { endpoint, headers }, models) {
5768
+ /**
5769
+ * The Promptfoo provider id and options that send `config` to the discovered endpoint.
5770
+ * `maxOutputTokens` replaces Promptfoo's default output cap and ambient `*_MAX_TOKENS`;
5771
+ * without it Promptfoo's default stands. Temperature is never sent: Promptfoo defaults it
5772
+ * to 0 (or ambient `*_TEMPERATURE`), which some models reject, so an `undefined` override
5773
+ * that is spread last drops the key from the request body.
5774
+ */
5775
+ function nativeProvider(config, snapshot, { endpoint, headers }, models, maxOutputTokens) {
5776
+ const noTemperature = { temperature: void 0 };
5756
5777
  const { model, reasoning_effort, response_format } = config;
5757
5778
  if (endpoint === "messages") {
5758
5779
  if (response_format !== void 0) throw new Error(`CopilotModel config.response_format is not supported for ${model}: Copilot serves it over the Anthropic Messages API, which has no JSON-object mode.`);
@@ -5762,6 +5783,9 @@ function nativeProvider(config, snapshot, { endpoint, headers }, models) {
5762
5783
  options: {
5763
5784
  config: {
5764
5785
  ...reasoning_effort === void 0 ? {} : anthropicReasoning(models, model, reasoning_effort),
5786
+ ...maxOutputTokens === void 0 ? {} : { max_tokens: maxOutputTokens },
5787
+ stream: true,
5788
+ extra_body: noTemperature,
5765
5789
  showThinking: false,
5766
5790
  apiBaseUrl: snapshot.baseUrl,
5767
5791
  apiKey: snapshot.apiKey,
@@ -5785,9 +5809,14 @@ function nativeProvider(config, snapshot, { endpoint, headers }, models) {
5785
5809
  if (endpoint === "chat/completions") return {
5786
5810
  id: `openai:chat:${model}`,
5787
5811
  options: { config: {
5788
- ...reasoning_effort === void 0 ? {} : {
5789
- reasoning_effort,
5790
- passthrough: { reasoning_effort }
5812
+ ...reasoning_effort === void 0 ? {} : { reasoning_effort },
5813
+ ...maxOutputTokens === void 0 ? {} : {
5814
+ max_tokens: maxOutputTokens,
5815
+ max_completion_tokens: maxOutputTokens
5816
+ },
5817
+ passthrough: {
5818
+ ...reasoning_effort === void 0 ? {} : { reasoning_effort },
5819
+ ...noTemperature
5791
5820
  },
5792
5821
  ...openai
5793
5822
  } }
@@ -5796,6 +5825,8 @@ function nativeProvider(config, snapshot, { endpoint, headers }, models) {
5796
5825
  id: `openai:responses:${model}`,
5797
5826
  options: { config: {
5798
5827
  ...reasoning_effort === void 0 ? {} : { reasoning_effort },
5828
+ ...maxOutputTokens === void 0 ? {} : { max_output_tokens: maxOutputTokens },
5829
+ passthrough: noTemperature,
5799
5830
  store: false,
5800
5831
  ...openai
5801
5832
  } }
@@ -5857,7 +5888,8 @@ function copilotReply(response, generation) {
5857
5888
  metadata: {
5858
5889
  ...reply.metadata,
5859
5890
  copilotEndpoint: generation.endpoint,
5860
- ...generation.prices ? { copilotTokenPrices: generation.prices } : {}
5891
+ ...generation.prices ? { copilotTokenPrices: generation.prices } : {},
5892
+ ...generation.maxOutputTokens === void 0 ? {} : { copilotMaxOutputTokens: generation.maxOutputTokens }
5861
5893
  }
5862
5894
  }, generation.endpoint);
5863
5895
  }
@@ -5897,7 +5929,8 @@ function createCopilotModel({ createEndpointClient = () => createCopilotEndpoint
5897
5929
  try {
5898
5930
  models = await owner.models();
5899
5931
  } catch {}
5900
- const { id, options } = nativeProvider(config, snapshot, route, models);
5932
+ const maxOutputTokens = config.max_tokens ?? modelOutputLimit(models, config.model);
5933
+ const { id, options } = nativeProvider(config, snapshot, route, models, maxOutputTokens);
5901
5934
  const { endpoint, refreshAt } = route;
5902
5935
  const prices = tokenPrices(models, config.model);
5903
5936
  const provider = await loadProvider(id, { options });
@@ -5907,6 +5940,7 @@ function createCopilotModel({ createEndpointClient = () => createCopilotEndpoint
5907
5940
  provider,
5908
5941
  endpoint,
5909
5942
  prices,
5943
+ maxOutputTokens,
5910
5944
  refreshAt,
5911
5945
  active: 0,
5912
5946
  retired: false