@meuecommerce/frete-adapter-node 0.3.0 → 0.4.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.
@@ -115,5 +115,36 @@ function createCorreiosHttpClient(options = {}) {
115
115
  getTime(payload, token) {
116
116
  return estimate("/prazo/v1/nacional", payload, token, "correios.getTime");
117
117
  },
118
+ async track(codes, token) {
119
+ if (!codes.length)
120
+ return [];
121
+ // SRO Rastro takes the codes as repeated `codigosObjetos` query params and
122
+ // `resultado=T` for the full event history (vs. `U` for the latest only).
123
+ // The language MUST go in the `Accept-Language` header (pt-BR/en/es-ES) —
124
+ // an `idioma` query param is rejected with SRO-018.
125
+ const params = new URLSearchParams();
126
+ for (const code of codes)
127
+ params.append("codigosObjetos", code);
128
+ params.append("resultado", "T");
129
+ const url = `${baseUrl}${frete_1.correiosSroPath}?${params.toString()}`;
130
+ const response = await fetchWithRetry(url, {
131
+ method: "GET",
132
+ headers: {
133
+ Accept: "application/json",
134
+ "Accept-Language": "pt-BR",
135
+ Authorization: `Bearer ${token}`,
136
+ },
137
+ });
138
+ if (response.status === 401) {
139
+ throw { status: 401, message: "unauthorized" };
140
+ }
141
+ if (response.status !== 200) {
142
+ const json = await response.json().catch(() => ({}));
143
+ logger.warn(`[correios.track] invalid response. status=${response.status} body=${JSON.stringify(json)}`);
144
+ return [];
145
+ }
146
+ const body = (await response.json());
147
+ return Array.isArray(body?.objetos) ? body.objetos : [];
148
+ },
118
149
  };
119
150
  }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCorreiosLabelHttpClient = createCorreiosLabelHttpClient;
4
+ /**
5
+ * Node implementation of the `CorreiosLabelClient` port using global `fetch`.
6
+ *
7
+ * Mirrors `correiosHttpClient.ts`: same base URL, per-request timeout, and
8
+ * bounded retry on transient network failures. It stays deliberately thin —
9
+ * one round-trip per method; the "request PDF then poll" loop lives in the
10
+ * `generateLabel` use case.
11
+ *
12
+ * Endpoints (Correios pré-postagem API):
13
+ * - `POST /prepostagem/v1/prepostagens` — create
14
+ * - `POST /prepostagem/v1/prepostagens/rotulo/assincrono/pdf` — request async PDF
15
+ * - `GET /prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}` — poll/download
16
+ *
17
+ * NOTE: the async download path isn't in the public docs mirror; the default
18
+ * below is the officially-referenced one and is overridable via `downloadPath`
19
+ * so it can be corrected against homolog without touching the use case.
20
+ */
21
+ const frete_1 = require("@meuecommerce/frete");
22
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
23
+ /**
24
+ * Some Correios label messages are transient: the pré-postagem is fine and the
25
+ * PDF is still being produced, so a later poll succeeds (e.g. PPN-291 "O rótulo
26
+ * ainda não foi gerado. Por favor, consulte novamente."). Distinguish those from
27
+ * terminal ones — notably PPN-288 "status Pendente" — which won't clear by
28
+ * polling and must be surfaced. Retryable → keep polling; terminal → `message`.
29
+ */
30
+ function isRetryableLabelMessage(msg) {
31
+ return /consulte novamente|ainda n[ãa]o foi gerado|n[ãa]o (foi )?gerado|PPN-291/i.test(msg);
32
+ }
33
+ function createCorreiosLabelHttpClient(options = {}) {
34
+ const { fetch = globalThis.fetch, baseUrl = frete_1.correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
35
+ if (typeof fetch !== "function") {
36
+ throw new Error("createCorreiosLabelHttpClient: no fetch available (Node 18+ or pass options.fetch)");
37
+ }
38
+ async function fetchWithRetry(url, init) {
39
+ let lastError;
40
+ for (let attempt = 0; attempt < retries; attempt++) {
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
43
+ try {
44
+ return await fetch(url, { ...init, signal: controller.signal });
45
+ }
46
+ catch (error) {
47
+ lastError = error;
48
+ if (attempt === retries - 1)
49
+ break;
50
+ await sleep(retryDelayMs * Math.pow(2, attempt));
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+ throw lastError;
57
+ }
58
+ function authHeaders(token) {
59
+ return {
60
+ "Content-Type": "application/json",
61
+ Accept: "application/json",
62
+ Authorization: `Bearer ${token}`,
63
+ };
64
+ }
65
+ return {
66
+ async createPrepostagem(payload, token) {
67
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens`, {
68
+ method: "POST",
69
+ headers: authHeaders(token),
70
+ body: JSON.stringify(payload),
71
+ });
72
+ // Correios returns a structured error body (with the failure reason) on 4xx;
73
+ // the use case filters by `txErro`, so pass it through rather than throwing.
74
+ const json = (await response.json().catch(() => ({})));
75
+ if (response.status !== 200 && response.status !== 201) {
76
+ logger.warn(`[correios.createPrepostagem] status=${response.status} body=${JSON.stringify(json)}`);
77
+ if (!json.txErro)
78
+ json.txErro = `http_${response.status}`;
79
+ }
80
+ return json;
81
+ },
82
+ async getPrepostagemStatus(id, token) {
83
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v2/prepostagens?id=${encodeURIComponent(id)}&page=0&size=1`, { method: "GET", headers: authHeaders(token) });
84
+ const json = (await response.json().catch(() => ({})));
85
+ const item = Array.isArray(json.itens) ? json.itens[0] : undefined;
86
+ return item ?? null;
87
+ },
88
+ async requestLabelPdf(ids, token) {
89
+ const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens/rotulo/assincrono/pdf`, {
90
+ method: "POST",
91
+ headers: authHeaders(token),
92
+ body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo }),
93
+ });
94
+ const json = (await response.json().catch(() => ({})));
95
+ if (!json.idRecibo)
96
+ throw new Error(`requestLabelPdf: no idRecibo (status=${response.status})`);
97
+ return { idRecibo: json.idRecibo };
98
+ },
99
+ async downloadLabelPdf(idRecibo, token) {
100
+ const path = downloadPath.replace("{idRecibo}", encodeURIComponent(idRecibo));
101
+ // tipoRotulo is also accepted as a query param on the download (PPN-285).
102
+ const sep = path.includes("?") ? "&" : "?";
103
+ const url = `${baseUrl}${path}${sep}tipoRotulo=${encodeURIComponent(tipoRotulo)}`;
104
+ const response = await fetchWithRetry(url, {
105
+ method: "GET",
106
+ headers: { Accept: "application/pdf, application/json", Authorization: `Bearer ${token}` },
107
+ });
108
+ const contentType = response.headers.get("content-type") ?? "";
109
+ // 202 (or 204) = still processing; tell the use case to poll again.
110
+ if (response.status === 202 || response.status === 204)
111
+ return { ready: false };
112
+ if (response.status !== 200) {
113
+ const body = await response.text().catch(() => "");
114
+ // A retryable business message can arrive with a non-2xx status too.
115
+ if (isRetryableLabelMessage(body))
116
+ return { ready: false };
117
+ return { ready: false, message: `http_${response.status}: ${body.slice(0, 200)}` };
118
+ }
119
+ // Correios may return the PDF as raw bytes (application/pdf) or wrapped in
120
+ // JSON as base64 (dados/pdf). Handle both.
121
+ if (contentType.includes("application/pdf") || contentType.includes("octet-stream")) {
122
+ const buf = Buffer.from(await response.arrayBuffer());
123
+ return buf.length ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
124
+ }
125
+ const json = (await response.json().catch(() => ({})));
126
+ const pdfBase64 = (json.dados ?? json.pdf ?? json.base64 ?? json.arquivo);
127
+ if (pdfBase64)
128
+ return { ready: true, pdfBase64 };
129
+ // 200 without a PDF carries a business message (e.g. "status Pendente" or
130
+ // "ainda não foi gerado, consulte novamente"). Retryable ones → keep polling.
131
+ const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
132
+ if (message && isRetryableLabelMessage(message))
133
+ return { ready: false };
134
+ return message ? { ready: false, message } : { ready: false };
135
+ },
136
+ };
137
+ }
@@ -6,7 +6,7 @@ class EnvSecretStore {
6
6
  prefix;
7
7
  constructor(options = {}) {
8
8
  this.env = options.env ?? process.env;
9
- this.prefix = options.prefix ?? "MEUFRETE_";
9
+ this.prefix = options.prefix ?? "FRETE_";
10
10
  }
11
11
  key(name) {
12
12
  return this.prefix + name.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase();
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryDimensionMemory = void 0;
4
+ /**
5
+ * In-memory `DimensionMemory` — local dev, the MCP server's local mode, and
6
+ * tests. Production swaps it for the Postgres-backed one; the core does not
7
+ * change.
8
+ *
9
+ * It enforces the layer rule rather than just storing what it is given: an
10
+ * agent guess (`L3`) must never overwrite what the merchant declared (`L0`).
11
+ * Putting that here as well as in the Postgres implementation is deliberate —
12
+ * it is a property of the memory, not of one backend, and the tests that pin it
13
+ * run against this one.
14
+ */
15
+ const frete_1 = require("@meuecommerce/frete");
16
+ class InMemoryDimensionMemory {
17
+ records = new Map();
18
+ constructor(seed = []) {
19
+ // O seed passa pela mesma regra de camada que uma escrita normal.
20
+ void this.set(seed);
21
+ }
22
+ /** How many products the memory holds. For tests and local inspection. */
23
+ get size() {
24
+ return this.records.size;
25
+ }
26
+ async get(identities) {
27
+ const found = new Map();
28
+ for (const identity of identities) {
29
+ const key = (0, frete_1.productKey)(identity);
30
+ if (!key)
31
+ continue;
32
+ const record = this.records.get(key.value);
33
+ if (record)
34
+ found.set(key.value, record);
35
+ }
36
+ return found;
37
+ }
38
+ async set(entries) {
39
+ for (const { identity, record } of entries) {
40
+ const key = (0, frete_1.productKey)(identity);
41
+ // Sem chave derivável não há o que gravar: inventar uma faria produtos
42
+ // sem relação compartilharem entrada.
43
+ if (!key)
44
+ continue;
45
+ const existing = this.records.get(key.value);
46
+ if (existing && !(0, frete_1.canOverwrite)(existing.source, record.source))
47
+ continue;
48
+ this.records.set(key.value, record);
49
+ }
50
+ }
51
+ async invalidate(identities) {
52
+ for (const identity of identities) {
53
+ const key = (0, frete_1.productKey)(identity);
54
+ if (key)
55
+ this.records.delete(key.value);
56
+ }
57
+ }
58
+ }
59
+ exports.InMemoryDimensionMemory = InMemoryDimensionMemory;
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createOpenAIBoxDistributor = exports.createOpenAIDimensionEstimator = exports.EnvSecretStore = exports.InMemorySettingsStore = exports.createCorreiosHttpClient = void 0;
3
+ exports.createOpenAIBoxDistributor = exports.PgDimensionMemory = exports.DIMENSION_MEMORY_SCHEMA_SQL = exports.createOpenAIResponsesAgentRunner = exports.createOpenAIDimensionEstimator = exports.DIMENSION_PROMPT_VERSION = exports.EnvSecretStore = exports.InMemoryDimensionMemory = exports.InMemorySettingsStore = exports.createCorreiosLabelHttpClient = exports.createCorreiosHttpClient = void 0;
4
4
  /**
5
5
  * @meuecommerce/frete-adapter-node — Node implementations of @meuecommerce/frete ports.
6
6
  *
@@ -9,11 +9,21 @@ exports.createOpenAIBoxDistributor = exports.createOpenAIDimensionEstimator = ex
9
9
  */
10
10
  var correiosHttpClient_js_1 = require("./correiosHttpClient.js");
11
11
  Object.defineProperty(exports, "createCorreiosHttpClient", { enumerable: true, get: function () { return correiosHttpClient_js_1.createCorreiosHttpClient; } });
12
+ var correiosLabelHttpClient_js_1 = require("./correiosLabelHttpClient.js");
13
+ Object.defineProperty(exports, "createCorreiosLabelHttpClient", { enumerable: true, get: function () { return correiosLabelHttpClient_js_1.createCorreiosLabelHttpClient; } });
12
14
  var inMemorySettingsStore_js_1 = require("./inMemorySettingsStore.js");
13
15
  Object.defineProperty(exports, "InMemorySettingsStore", { enumerable: true, get: function () { return inMemorySettingsStore_js_1.InMemorySettingsStore; } });
16
+ var inMemoryDimensionMemory_js_1 = require("./inMemoryDimensionMemory.js");
17
+ Object.defineProperty(exports, "InMemoryDimensionMemory", { enumerable: true, get: function () { return inMemoryDimensionMemory_js_1.InMemoryDimensionMemory; } });
14
18
  var envSecretStore_js_1 = require("./envSecretStore.js");
15
19
  Object.defineProperty(exports, "EnvSecretStore", { enumerable: true, get: function () { return envSecretStore_js_1.EnvSecretStore; } });
16
20
  var openAIDimensionEstimator_js_1 = require("./openAIDimensionEstimator.js");
21
+ Object.defineProperty(exports, "DIMENSION_PROMPT_VERSION", { enumerable: true, get: function () { return openAIDimensionEstimator_js_1.DIMENSION_PROMPT_VERSION; } });
17
22
  Object.defineProperty(exports, "createOpenAIDimensionEstimator", { enumerable: true, get: function () { return openAIDimensionEstimator_js_1.createOpenAIDimensionEstimator; } });
23
+ var openAIResponsesAgentRunner_js_1 = require("./openAIResponsesAgentRunner.js");
24
+ Object.defineProperty(exports, "createOpenAIResponsesAgentRunner", { enumerable: true, get: function () { return openAIResponsesAgentRunner_js_1.createOpenAIResponsesAgentRunner; } });
25
+ var pgDimensionMemory_js_1 = require("./pgDimensionMemory.js");
26
+ Object.defineProperty(exports, "DIMENSION_MEMORY_SCHEMA_SQL", { enumerable: true, get: function () { return pgDimensionMemory_js_1.DIMENSION_MEMORY_SCHEMA_SQL; } });
27
+ Object.defineProperty(exports, "PgDimensionMemory", { enumerable: true, get: function () { return pgDimensionMemory_js_1.PgDimensionMemory; } });
18
28
  var openAIBoxDistributor_js_1 = require("./openAIBoxDistributor.js");
19
29
  Object.defineProperty(exports, "createOpenAIBoxDistributor", { enumerable: true, get: function () { return openAIBoxDistributor_js_1.createOpenAIBoxDistributor; } });
@@ -1,31 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DIMENSION_PROMPT_VERSION = void 0;
3
4
  exports.createOpenAIDimensionEstimator = createOpenAIDimensionEstimator;
4
- const ESTIMATE_DIMENSIONS_TOOL = {
5
- type: "function",
6
- function: {
7
- name: "estimate_dimensions",
8
- description: "Estima dimensões de produtos PRESERVANDO os nomes exatos fornecidos",
9
- parameters: {
10
- type: "object",
11
- properties: {
12
- products: {
13
- type: "array",
14
- items: {
15
- type: "object",
16
- properties: {
17
- name: { type: "string" },
18
- length: { type: "number", description: "Comprimento em cm" },
19
- width: { type: "number", description: "Largura em cm" },
20
- height: { type: "number", description: "Altura em cm" },
21
- },
22
- required: ["name", "length", "width", "height"],
23
- },
5
+ const openAIResponsesAgentRunner_js_1 = require("./openAIResponsesAgentRunner.js");
6
+ /**
7
+ * Bump whenever the prompt or the schema changes. It is written next to every
8
+ * `L3` record so a prompt change can invalidate stale estimates in background
9
+ * instead of at checkout.
10
+ */
11
+ exports.DIMENSION_PROMPT_VERSION = "2026-09-04.responses.v1";
12
+ /**
13
+ * Strict JSON schema for the response.
14
+ *
15
+ * `strict: true` has two requirements that are easy to miss and reject the
16
+ * whole request when absent: every object needs `additionalProperties: false`,
17
+ * and `required` has to list every property — optional fields are not allowed.
18
+ */
19
+ const DIMENSIONS_SCHEMA = {
20
+ type: "object",
21
+ properties: {
22
+ products: {
23
+ type: "array",
24
+ description: "Uma entrada por produto pedido, na mesma ordem.",
25
+ items: {
26
+ type: "object",
27
+ properties: {
28
+ name: { type: "string", description: "O nome EXATO recebido, sem reescrever" },
29
+ length: { type: "number", description: "Comprimento em cm" },
30
+ width: { type: "number", description: "Largura em cm" },
31
+ height: { type: "number", description: "Altura em cm" },
24
32
  },
33
+ required: ["name", "length", "width", "height"],
34
+ additionalProperties: false,
25
35
  },
26
- required: ["products"],
27
36
  },
28
37
  },
38
+ required: ["products"],
39
+ additionalProperties: false,
29
40
  };
30
41
  function largestBox(boxes) {
31
42
  return boxes.reduce((largest, current) => current.width * current.height * current.length > largest.width * largest.height * largest.length ? current : largest);
@@ -43,8 +54,7 @@ function createPrompt(products, boxes, samples) {
43
54
  `🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
44
55
  `📦 PRODUTOS PARA ESTIMAR:\n${products.map((p, i) => `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`).join("\n")}\n` +
45
56
  samplesSection +
46
- `\n⚠️ REGRAS: respeite os limites; preserve os nomes EXATOS; retorne length×width×height em cm.\n` +
47
- `Responda usando a função 'estimate_dimensions'.`);
57
+ `\n⚠️ REGRAS: respeite os limites; preserve os nomes EXATOS; retorne length×width×height em cm.`);
48
58
  }
49
59
  /** Apply the largest-box physical limits + a minimum-volume floor to AI output. */
50
60
  function validateAndOptimize(aiResult, products, boxes) {
@@ -79,52 +89,32 @@ function validateAndOptimize(aiResult, products, boxes) {
79
89
  return { products: optimized };
80
90
  }
81
91
  function createOpenAIDimensionEstimator(options) {
82
- const { apiKey, fetch = globalThis.fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl = "https://api.openai.com/v1/chat/completions", timeoutMs = 20000, } = options;
83
- if (!apiKey)
84
- throw new Error("createOpenAIDimensionEstimator: apiKey is required");
85
- if (typeof fetch !== "function")
86
- throw new Error("createOpenAIDimensionEstimator: no fetch available");
92
+ const { apiKey, fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl, timeoutMs, runner } = options;
93
+ const agent = runner ??
94
+ (0, openAIResponsesAgentRunner_js_1.createOpenAIResponsesAgentRunner)({
95
+ apiKey: apiKey ?? "",
96
+ ...(fetch ? { fetch } : {}),
97
+ model,
98
+ ...(apiUrl ? { apiUrl } : {}),
99
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
100
+ });
87
101
  return {
88
102
  async estimate(products, boxes, productSamples = []) {
89
103
  if (!boxes.length)
90
104
  throw new Error("estimate: no boxes provided");
91
- const body = {
92
- model,
93
- messages: [
94
- {
95
- role: "system",
96
- content: "Você é um especialista em dimensões de produtos. Use os exemplos personalizados como prioridade máxima quando o nome bater.",
97
- },
98
- { role: "user", content: createPrompt(products, boxes, productSamples) },
99
- ],
100
- tools: [ESTIMATE_DIMENSIONS_TOOL],
101
- tool_choice: { type: "function", function: { name: "estimate_dimensions" } },
105
+ const result = await agent.run({
106
+ task: "estimate-dimensions",
107
+ promptVersion: exports.DIMENSION_PROMPT_VERSION,
108
+ system: "Você é um especialista em dimensões de produtos. Use os exemplos personalizados como prioridade máxima quando o nome bater.",
109
+ user: createPrompt(products, boxes, productSamples),
110
+ schema: { name: "product_dimensions", schema: DIMENSIONS_SCHEMA },
102
111
  temperature,
103
- };
104
- const controller = new AbortController();
105
- const timer = setTimeout(() => controller.abort(), timeoutMs);
106
- let response;
107
- try {
108
- response = await fetch(apiUrl, {
109
- method: "POST",
110
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
111
- body: JSON.stringify(body),
112
- signal: controller.signal,
113
- });
112
+ });
113
+ if (!Array.isArray(result.output?.products)) {
114
+ throw new Error("Resposta da IA sem a lista de produtos");
114
115
  }
115
- finally {
116
- clearTimeout(timer);
117
- }
118
- if (!response.ok)
119
- throw new Error(`OpenAI API Error: ${response.status} ${response.statusText}`);
120
- const json = (await response.json());
121
- if (json.error)
122
- throw new Error(`OpenAI Error: ${json.error.message}`);
123
- const args = json.choices?.[0]?.message?.tool_calls?.[0]?.function?.arguments;
124
- if (!args)
125
- throw new Error("IA não conseguiu estimar dimensões dos produtos");
126
- const parsed = JSON.parse(args);
127
- return validateAndOptimize(parsed, products, boxes);
116
+ const validated = validateAndOptimize(result.output, products, boxes);
117
+ return result.usage ? { ...validated, usage: result.usage } : validated;
128
118
  },
129
119
  };
130
120
  }
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractOutputText = extractOutputText;
4
+ exports.findRefusal = findRefusal;
5
+ exports.createOpenAIResponsesAgentRunner = createOpenAIResponsesAgentRunner;
6
+ /** The model's text, accepting both shapes the Responses API delivers it in. */
7
+ function extractOutputText(json) {
8
+ if (typeof json.output_text === "string" && json.output_text.trim())
9
+ return json.output_text;
10
+ const parts = [];
11
+ for (const item of json.output ?? []) {
12
+ for (const content of item.content ?? []) {
13
+ if (typeof content.text === "string")
14
+ parts.push(content.text);
15
+ }
16
+ }
17
+ return parts.join("").trim();
18
+ }
19
+ /**
20
+ * A refusal is a *successful* response in which the model declined to fill the
21
+ * schema. Without this check it would read as empty text and be reported as
22
+ * "the model could not answer", hiding the real reason.
23
+ */
24
+ function findRefusal(json) {
25
+ for (const item of json.output ?? []) {
26
+ for (const content of item.content ?? []) {
27
+ if (typeof content.refusal === "string" && content.refusal)
28
+ return content.refusal;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+ function createOpenAIResponsesAgentRunner(options) {
34
+ const { apiKey, fetch = globalThis.fetch, model: defaultModel = "gpt-4o-mini", apiUrl = "https://api.openai.com/v1/responses", timeoutMs = 20000, } = options;
35
+ if (!apiKey)
36
+ throw new Error("createOpenAIResponsesAgentRunner: apiKey is required");
37
+ if (typeof fetch !== "function")
38
+ throw new Error("createOpenAIResponsesAgentRunner: no fetch available");
39
+ return {
40
+ async run(request) {
41
+ const input = [];
42
+ if (request.system)
43
+ input.push({ role: "system", content: request.system });
44
+ // `images` ainda não é usado pelo estimador; quando a fase 2 ligar, o
45
+ // conteúdo do turno do usuário vira lista em vez de string.
46
+ if (request.images?.length) {
47
+ input.push({
48
+ role: "user",
49
+ content: [
50
+ { type: "input_text", text: request.user },
51
+ ...request.images.map((url) => ({ type: "input_image", image_url: url })),
52
+ ],
53
+ });
54
+ }
55
+ else {
56
+ input.push({ role: "user", content: request.user });
57
+ }
58
+ const body = {
59
+ model: defaultModel,
60
+ input,
61
+ text: {
62
+ format: {
63
+ type: "json_schema",
64
+ name: request.schema.name,
65
+ strict: true,
66
+ schema: request.schema.schema,
67
+ },
68
+ },
69
+ };
70
+ if (request.temperature !== undefined)
71
+ body.temperature = request.temperature;
72
+ if (request.maxOutputTokens !== undefined)
73
+ body.max_output_tokens = request.maxOutputTokens;
74
+ const controller = new AbortController();
75
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
76
+ let response;
77
+ try {
78
+ response = await fetch(apiUrl, {
79
+ method: "POST",
80
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
81
+ body: JSON.stringify(body),
82
+ signal: controller.signal,
83
+ });
84
+ }
85
+ finally {
86
+ clearTimeout(timer);
87
+ }
88
+ if (!response.ok)
89
+ throw new Error(`OpenAI API Error: ${response.status} ${response.statusText}`);
90
+ const json = (await response.json());
91
+ if (json.error)
92
+ throw new Error(`OpenAI Error: ${json.error.message}`);
93
+ const refusal = findRefusal(json);
94
+ if (refusal)
95
+ throw new Error(`OpenAI recusou a resposta de ${request.task}: ${refusal}`);
96
+ const text = extractOutputText(json);
97
+ if (!text)
98
+ throw new Error(`Resposta vazia da IA para ${request.task}`);
99
+ let output;
100
+ try {
101
+ output = JSON.parse(text);
102
+ }
103
+ catch {
104
+ throw new Error(`Resposta da IA para ${request.task} não é JSON válido`);
105
+ }
106
+ const model = json.model ?? defaultModel;
107
+ const usage = json.usage
108
+ ? {
109
+ model,
110
+ ...(typeof json.usage.input_tokens === "number" ? { inputTokens: json.usage.input_tokens } : {}),
111
+ ...(typeof json.usage.output_tokens === "number" ? { outputTokens: json.usage.output_tokens } : {}),
112
+ }
113
+ : undefined;
114
+ // Sempre 1 nesta fase: a repescagem por falha de validação é o próximo
115
+ // passo, e é ela que vai fazer este número variar.
116
+ return usage ? { output, usage, model, attempts: 1 } : { output, model, attempts: 1 };
117
+ },
118
+ };
119
+ }