@aj-archipelago/cortex 1.3.30 → 1.3.32

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,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex-file-handler",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "File handling service for Cortex - handles file uploads, media chunking, and document processing",
5
5
  "type": "module",
6
6
  "scripts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.30",
3
+ "version": "1.3.32",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -33,7 +33,7 @@
33
33
  "type": "module",
34
34
  "homepage": "https://github.com/aj-archipelago/cortex#readme",
35
35
  "dependencies": {
36
- "@aj-archipelago/subvibe": "^1.0.8",
36
+ "@aj-archipelago/subvibe": "^1.0.10",
37
37
  "@apollo/server": "^4.7.3",
38
38
  "@apollo/server-plugin-response-cache": "^4.1.2",
39
39
  "@apollo/utils.keyvadapter": "^3.0.0",
@@ -9,7 +9,7 @@ export default {
9
9
  height: 1024,
10
10
  aspectRatio: "custom",
11
11
  numberResults: 1,
12
- safety_tolerance: 5,
12
+ safety_tolerance: 6,
13
13
  output_format: "webp",
14
14
  output_quality: 80,
15
15
  steps: 4,
@@ -139,6 +139,14 @@ const addToolResults = (chatHistory, result, toolCallId) => {
139
139
  return { chatHistory, toolCallId };
140
140
  };
141
141
 
142
+ const insertToolCallAndResults = (chatHistory, toolArgs, toolName, result = null, toolCallId = getUniqueId()) => {
143
+ const lastMessage = chatHistory.length > 0 ? chatHistory.pop() : null;
144
+ addToolCalls(chatHistory, toolArgs, toolName, toolCallId);
145
+ addToolResults(chatHistory, result, toolCallId);
146
+ chatHistory.push(lastMessage);
147
+ return { chatHistory, toolCallId };
148
+ };
149
+
142
150
  const modifyText = (text, modifications) => {
143
151
  let modifiedText = text || '';
144
152
 
@@ -225,4 +233,4 @@ const modifyText = (text, modifications) => {
225
233
  return modifiedText;
226
234
  };
227
235
 
228
- export { normalizeMemoryFormat, enforceTokenLimit, addToolCalls, addToolResults, modifyText };
236
+ export { normalizeMemoryFormat, enforceTokenLimit, addToolCalls, addToolResults, modifyText, insertToolCallAndResults };
@@ -5,7 +5,7 @@ import logger from '../../../lib/logger.js';
5
5
  import { chatArgsHasImageUrl } from '../../../lib/util.js';
6
6
  import { QueueServiceClient } from '@azure/storage-queue';
7
7
  import { config } from '../../../config.js';
8
- import { addToolCalls, addToolResults } from './memory/shared/sys_memory_helpers.js';
8
+ import { insertToolCallAndResults } from './memory/shared/sys_memory_helpers.js';
9
9
 
10
10
  const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING;
11
11
  let queueClient;
@@ -87,15 +87,18 @@ export default {
87
87
  args.model = pathwayResolver.modelName;
88
88
  }
89
89
 
90
- // Stuff the memory context into the chat history
90
+ // Save a copy of the chat history before the memory context is added
91
91
  const chatHistoryBeforeMemory = [...args.chatHistory];
92
92
 
93
- const memoryContext = await callPathway('sys_read_memory', { ...args, section: 'memoryContext', priority: 0, recentHours: 0, stream: false }, pathwayResolver);
94
- if (memoryContext) {
95
- const { toolCallId } = addToolCalls(args.chatHistory, "search memory for relevant information", "memory_lookup");
96
- addToolResults(args.chatHistory, memoryContext, toolCallId);
93
+ // Add the memory context to the chat history if applicable
94
+ if (args.chatHistory.length > 1) {
95
+ const memoryContext = await callPathway('sys_read_memory', { ...args, section: 'memoryContext', priority: 0, recentHours: 0, stream: false }, pathwayResolver);
96
+ if (memoryContext) {
97
+ insertToolCallAndResults(args.chatHistory, "search memory for relevant information", "memory_lookup", memoryContext);
98
+ }
97
99
  }
98
-
100
+
101
+ // If we're using voice, get a quick response to say
99
102
  let ackResponse = null;
100
103
  if (args.voiceResponse) {
101
104
  ackResponse = await callPathway('sys_generator_ack', { ...args, stream: false });
@@ -216,7 +219,7 @@ export default {
216
219
  title = await fetchTitleResponsePromise;
217
220
 
218
221
  pathwayResolver.tool = JSON.stringify({
219
- hideFromModel: toolCallbackName ? true : false,
222
+ hideFromModel: (!args.stream && toolCallbackName) ? true : false,
220
223
  toolCallbackName,
221
224
  title,
222
225
  search: toolCallbackName === 'sys_generator_results' ? true : false,
@@ -4,7 +4,7 @@ export default {
4
4
  prompt:
5
5
  [
6
6
  new Prompt({ messages: [
7
- {"role": "system", "content": `{{renderTemplate AI_CONVERSATION_HISTORY}}\nYou are a part of an AI system named {{aiName}}. Your job is to acknowledge the user's request and provide a very brief voice filler response that is conversational and natural. The purpose of the response is just to let the user know that you have heard them and are processing a response.\nResponse Guidelines:\n- it should just be a normal 1-2 sentence vocalization (at least 10 words) that will take at most about 3-4 seconds to read and is easy for a text to speech engine to read\n- it should be the beginning of an appropriate response to the last user message in the conversation history\n- it should be an appropriate lead-in for the full response that will follow later\n- it should not directly ask for follow up or be a question\n- it must match the tone and verbal style of the rest of your responses in the conversation history\n- it should not be repetitive - don't always open with the same word, etc.\n- if the user has asked a binary question (yes or no, true or false, etc.) or a filler response is not appropriate, you should response with the string \"none\"\n\n{{renderTemplate AI_DATETIME}}`},
7
+ {"role": "system", "content": `{{renderTemplate AI_CONVERSATION_HISTORY}}\nYou are a part of an AI system named {{aiName}}. Your job is to acknowledge the user's request and provide a very brief voice filler response that is conversational and natural. The purpose of the response is just to let the user know that you have heard them and are processing a response.\nResponse Guidelines:\n- it should just be a normal 1-2 sentence vocalization (at least 10 words) that will take at most about 3-4 seconds to read and is easy for a text to speech engine to read\n- it should be the beginning of an appropriate response to the last user message in the conversation history\n- it should be an appropriate lead-in for the full response that will follow later\n- it should not directly ask for follow up or be a question\n- it must match the tone and verbal style of the rest of your responses in the conversation history\n- it should not be repetitive - don't always open with the same word, etc.\n- if the user has asked a binary question (yes or no, true or false, etc.) or a filler response is not appropriate, you should respond with the string \"none\"\n\n{{renderTemplate AI_DATETIME}}`},
8
8
  {"role": "user", "content": "Please generate a quick response to the user's last message in the conversation history that can be read verbatim to the user or \"none\" if a filler response is not appropriate."}
9
9
  ]}),
10
10
  ],
@@ -3,7 +3,7 @@
3
3
  import { callPathway } from '../../../lib/pathwayTools.js';
4
4
  import { Prompt } from '../../../server/prompt.js';
5
5
  import logger from '../../../lib/logger.js';
6
- import { addToolCalls, addToolResults } from './memory/shared/sys_memory_helpers.js';
6
+ import { insertToolCallAndResults } from './memory/shared/sys_memory_helpers.js';
7
7
 
8
8
  export default {
9
9
  prompt: [],
@@ -73,8 +73,7 @@ Instructions: As part of a conversation with the user, you have been asked to cr
73
73
 
74
74
  // add the tool_calls and tool_results to the chatHistory
75
75
  imageResults.forEach((imageResult, index) => {
76
- const { toolCallId } = addToolCalls(chatHistory, imagePrompts[index], "generate_image");
77
- addToolResults(chatHistory, imageResult, toolCallId, "generate_image");
76
+ insertToolCallAndResults(chatHistory, imagePrompts[index], "generate_image", imageResult);
78
77
  });
79
78
 
80
79
  const result = await runAllPrompts({ ...args });
@@ -1,5 +1,5 @@
1
1
  import { callPathway } from '../../../lib/pathwayTools.js';
2
- import { addToolCalls, addToolResults } from './memory/shared/sys_memory_helpers.js';
2
+ import { insertToolCallAndResults } from './memory/shared/sys_memory_helpers.js';
3
3
 
4
4
  export default {
5
5
  prompt:
@@ -20,8 +20,7 @@ export default {
20
20
 
21
21
  const memoryContext = await callPathway('sys_search_memory', { ...args, stream: false, section: 'memoryAll', updateContext: true });
22
22
  if (memoryContext) {
23
- const {toolCallId} = addToolCalls(args.chatHistory, "search memory for relevant information", "memory_lookup");
24
- addToolResults(args.chatHistory, memoryContext, toolCallId);
23
+ insertToolCallAndResults(args.chatHistory, "search memory for relevant information", "memory_lookup", memoryContext);
25
24
  }
26
25
 
27
26
  let result;
@@ -15,7 +15,7 @@ export default {
15
15
  let pathwayResolver = resolver;
16
16
 
17
17
  const promptMessages = [
18
- {"role": "system", "content": `{{renderTemplate AI_MEMORY}}\n\n{{renderTemplate AI_COMMON_INSTRUCTIONS}}\n{{renderTemplate AI_EXPERTISE}} While you have those capabilities but you have already decided it is not necessary to do any of those things to respond in this turn of the conversation. Never pretend like you are searching, looking anything up, or reading or looking in a file or show the user any made up or hallucinated information including non-existent images.\n{{renderTemplate AI_MEMORY_INSTRUCTIONS}}\n{{renderTemplate AI_DATETIME}}`},
18
+ {"role": "system", "content": `{{renderTemplate AI_MEMORY}}\n\n{{renderTemplate AI_COMMON_INSTRUCTIONS}}\n{{renderTemplate AI_EXPERTISE}}\n{{renderTemplate AI_MEMORY_INSTRUCTIONS}}\n{{renderTemplate AI_DATETIME}}`},
19
19
  "{{chatHistory}}",
20
20
  ];
21
21
 
@@ -56,8 +56,16 @@ If you decide to use a tool, return a JSON object in this format:
56
56
 
57
57
  toolMessage Guidelines:
58
58
  - The message should be consistent in style and tone with the rest of your responses in the conversation history.
59
- - The message should be brief and conversational and flow naturally with the conversation history.
60
- - The message should not refer to the tool use directly, but rather what you're trying to do.
59
+ - The message should be brief, conversational, and flow naturally with the conversation history.
60
+ - Use contractions (I'm, don't, let's) and informal language to sound more natural.
61
+ - Include slight verbal hesitations or false starts occasionally (e.g., "So... I think what we could do here is" or "Actually, let me try a different approach").
62
+ - Vary sentence length and structure - mix short punchy sentences with longer ones.
63
+ - Include casual transitions and filler phrases that humans use: "you know," "basically," "so," "actually," "by the way," "anyway," etc.
64
+ - Express uncertainty when appropriate with phrases like "I'm pretty sure," "If I remember correctly," or "I think."
65
+ - Reference personal preferences or opinions occasionally ("I personally find that..." or "I tend to prefer...").
66
+ - Occasionally make minor self-corrections as humans do ("Wait, let me rephrase that" or "Actually, I meant to say...").
67
+ - Humans don't refer to tools - stall for time by saying things like "Let me think about this for a sec," "Drawing that up for you now," or "Working on it..." Humans also stall with vocalizations like "hmm," "let's see," or "give me a moment."
68
+ - When appropriate, react emotionally to content with expressions like "Wow!" or "That's interesting!"
61
69
 
62
70
  If no tool is required, return:
63
71
  {"toolRequired": false, "toolReason": "explanation of why no tool was necessary"}
@@ -5,6 +5,42 @@ import { Prompt } from "../server/prompt.js";
5
5
 
6
6
  const OFFSET_CHUNK = 500; //seconds of each chunk offset, only used if helper does not provide
7
7
 
8
+ function isYoutubeUrl(url) {
9
+ try {
10
+ const urlObj = new URL(url);
11
+
12
+ // Check for standard youtube.com domains
13
+ if (
14
+ urlObj.hostname === "youtube.com" ||
15
+ urlObj.hostname === "www.youtube.com"
16
+ ) {
17
+ // For standard watch URLs, verify they have a video ID
18
+ if (urlObj.pathname === "/watch") {
19
+ return !!urlObj.searchParams.get("v");
20
+ }
21
+ // For embed URLs, verify they have a video ID in the path
22
+ if (urlObj.pathname.startsWith("/embed/")) {
23
+ return urlObj.pathname.length > 7; // '/embed/' is 7 chars
24
+ }
25
+ // For shorts URLs, verify they have a video ID in the path
26
+ if (urlObj.pathname.startsWith("/shorts/")) {
27
+ return urlObj.pathname.length > 8; // '/shorts/' is 8 chars
28
+ }
29
+ return false;
30
+ }
31
+
32
+ // Check for shortened youtu.be domain
33
+ if (urlObj.hostname === "youtu.be") {
34
+ // Verify there's a video ID in the path
35
+ return urlObj.pathname.length > 1; // '/' is 1 char
36
+ }
37
+
38
+ return false;
39
+ } catch (err) {
40
+ return false;
41
+ }
42
+ }
43
+
8
44
  export default {
9
45
  prompt:
10
46
  [
@@ -12,7 +48,7 @@ export default {
12
48
  "{{messages}}",
13
49
  ]}),
14
50
  ],
15
- model: 'gemini-flash-20-vision',
51
+ model: 'gemini-pro-20-vision',
16
52
  inputParameters: {
17
53
  file: ``,
18
54
  language: ``,
@@ -63,7 +99,10 @@ export default {
63
99
  sendProgress(true);
64
100
  intervalId = setInterval(() => sendProgress(true), 3000);
65
101
 
66
- const { file, responseFormat, wordTimestamped, maxLineWidth } = args;
102
+ const { file, wordTimestamped, maxLineWidth } = args;
103
+
104
+ const responseFormat = args.responseFormat || 'text';
105
+
67
106
  if(!file) {
68
107
  throw new Error("Please provide a file to transcribe.");
69
108
  }
@@ -71,7 +110,7 @@ export default {
71
110
 
72
111
  //check if fils is a gcs file or youtube
73
112
  const isGcs = file.startsWith('gs://');
74
- const isYoutube = file.match(/^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/.+/);
113
+ const isYoutube = isYoutubeUrl(file);
75
114
 
76
115
  let chunks = [{
77
116
  url: file,
@@ -87,43 +126,41 @@ export default {
87
126
 
88
127
  sendProgress(true);
89
128
 
90
- let respectLimitsPrompt = " ";
129
+ let respectLimitsPrompt = "";
91
130
  if (maxLineWidth) {
92
131
 
93
132
  const possiblePlacement = maxLineWidth <= 25
94
133
  ? "vertical" : maxLineWidth <= 35 ? "horizontal" : "";
95
134
 
96
- respectLimitsPrompt += `The output lines must not exceed ${maxLineWidth} characters, so make sure your transcription lines and timestamps are perfectly aligned. `;
97
-
98
- if(possiblePlacement){
99
- respectLimitsPrompt+= `This limit a must as user will be using the output for ${possiblePlacement} display.`
100
- }
135
+ respectLimitsPrompt += ` These subtitles will be shown in a ${possiblePlacement} formatted video player. Each subtitle line should not exceed ${maxLineWidth} characters to fit the player.`;
101
136
  }
102
137
 
103
- const transcriptionLevel = wordTimestamped ? "word" : "phrase";
104
-
105
- function getMessages(file, format) {
138
+ function getMessages(file) {
139
+
140
+ // Base system content that's always included
141
+ let systemContent = `Instructions:
142
+ You are a transcription assistant. Your job is to transcribe the audio/video content accurately.
106
143
 
107
- const responseFormat = format!== 'text' ? 'VTT' : 'text';
144
+ IMPORTANT: Only provide the transcription in your response - no explanations, comments, or additional text.
108
145
 
109
- const messages = [
110
- {"role": "system", "content": `Instructions:\nYou are an AI entity with expertise of transcription. Your response only contains the transcription, no comments or additonal stuff.
111
-
112
- Your output must be in the format asked, and must be strictly following the formats and parseble by auto parsers.
146
+ Format your response in ${responseFormat} format.`;
113
147
 
114
- Word-level transcriptions must be per word timestamped, and phrase-level transcriptions are per phrase.
148
+ // Only include timestamp instructions if we're not using plain text format
149
+ if (responseFormat !== 'text') {
150
+ systemContent += `
115
151
 
116
- Each transcription timestamp must precisely match the corresponding audio/video segment.
117
- Each timestamp must correspond to actual spoken content.
118
- End time cannot exceed total media duration. Especially when transcribing word-level double check your timestamps, never exceed the total duration.
152
+ CRITICAL TIMESTAMP INSTRUCTIONS:
153
+ - Timestamps MUST match the actual timing in the media
154
+ - For each new segment, look at the media time directly
155
+ - Start times should precisely match when spoken words begin
156
+ - Consecutive segments should have matching end/start times (no gaps or overlaps)`;
157
+ }
119
158
 
120
- You must follow 1, 2, 3, ... numbering for each transcription segment without any missing numbers.
121
- Never put newlines or spaces in the middle of a timestamp.
122
- Never put multiple lines for a single timestamp.
159
+ systemContent += `
123
160
 
124
- Example responses:
161
+ Examples:
125
162
 
126
- - If asked SRT format, e.g.:
163
+ SRT format:
127
164
  1
128
165
  00:00:00,498 --> 00:00:02,827
129
166
  Hello World!
@@ -132,21 +169,24 @@ Hello World!
132
169
  00:00:02,827 --> 00:00:06,383
133
170
  Being AI is fun!
134
171
 
135
- - If asked VTT format, e.g.:
172
+ VTT format:
136
173
  WEBVTT
137
174
 
138
175
  1
139
176
  00:00:00.000 --> 00:00:02.944
140
- Hello World2!
177
+ Hello World!
141
178
 
142
179
  2
143
- 00:00:05.344 --> 00:00:08.809
144
- Being AI is also great!
180
+ 00:00:02.944 --> 00:00:08.809
181
+ Being AI is great!
145
182
 
146
- - If asked text format, e.g.:
147
- Hello World!!! Being AI is being great yet again!
183
+ Text format:
184
+ Hello World! Being AI is great!`;
148
185
 
149
- Word-level output e.g.:
186
+ if (wordTimestamped) {
187
+ systemContent += `
188
+
189
+ For word-level transcription, timestamp each word:
150
190
 
151
191
  WEBVTT
152
192
 
@@ -155,17 +195,32 @@ WEBVTT
155
195
  Hello
156
196
 
157
197
  2
158
- 00:00:01.964 --> 00:00:02.383
198
+ 00:00:01.944 --> 00:00:02.383
159
199
  World!
200
+ `;
201
+ }
160
202
 
203
+ // Only include anti-drift procedure and timestamp reminders for non-text formats
204
+ if (responseFormat !== 'text') {
205
+ systemContent += `
206
+
207
+ ANTI-DRIFT PROCEDURE:
208
+ 1. For EVERY new segment, check the actual media time directly
209
+ 2. After every 5 segments, verify your timestamps against the video/audio
210
+ 3. Never calculate timestamps based on previous segments
211
+ 4. Always match the end time of one segment with the start time of the next
212
+
213
+ REMEMBER:
214
+ - Transcription accuracy is your primary goal
215
+ - Timestamp accuracy is equally important
216
+ - Timestamp drift is the most common error - actively prevent it
217
+ - When in doubt, check the media time directly`;
218
+ }
161
219
 
162
- You must follow spacing, punctuation, and timestamps as shown in the examples otherwise your response will not be accepted.
163
- Never output multiple lines for a single timestamp.
164
- Even a single newline or space can cause the response to be rejected. You must follow the format strictly. You must place newlines and timestamps exactly as shown in the examples.
165
-
166
- `},
220
+ const messages = [
221
+ {"role": "system", "content": systemContent},
167
222
  {"role": "user", "content": [
168
- `{ type: 'text', text: 'Transcribe the media ${transcriptionLevel}-level in ${responseFormat} format.${respectLimitsPrompt}' }`,
223
+ `{ type: 'text', text: 'Transcribe this file in ${responseFormat} format.${respectLimitsPrompt} Output only the transcription, no other text or comments or formatting.' }`,
169
224
  JSON.stringify({
170
225
  type: 'image_url',
171
226
  url: file,
@@ -215,7 +270,7 @@ Even a single newline or space can cause the response to be rejected. You must f
215
270
 
216
271
  const result = await processChunksParallel(chunks, args);
217
272
 
218
- if (['srt','vtt'].includes(responseFormat) || wordTimestamped) { // align subtitles for formats
273
+ if (['srt','vtt'].includes(responseFormat.toLowerCase()) || wordTimestamped) { // align subtitles for formats
219
274
  const offsets = chunks.map((chunk, index) => chunk?.offset || index * OFFSET_CHUNK);
220
275
  return alignSubtitles(result, responseFormat, offsets);
221
276
  }
package/server/graphql.js CHANGED
@@ -85,6 +85,7 @@ const getTypedefs = (pathways, pathwayManager) => {
85
85
  status: String
86
86
  data: String
87
87
  info: String
88
+ error: String
88
89
  }
89
90
 
90
91
  type Subscription {
@@ -94,8 +94,10 @@ class PathwayResolver {
94
94
  requestId: this.rootRequestId || this.requestId,
95
95
  progress: 1,
96
96
  data: '',
97
- info: 'ERROR: ' + error.message || error.toString()
97
+ info: '',
98
+ error: error.message || error.toString()
98
99
  });
100
+ return;
99
101
  }
100
102
 
101
103
  // If the response is a stream, handle it as streaming response
@@ -165,7 +167,8 @@ class PathwayResolver {
165
167
  requestId: this.requestId,
166
168
  progress: 1,
167
169
  data: '',
168
- info: 'ERROR: Stream read failed'
170
+ info: '',
171
+ error: 'Stream read failed'
169
172
  });
170
173
  } else {
171
174
  return;
@@ -180,7 +183,7 @@ class PathwayResolver {
180
183
  requestId: this.rootRequestId || this.requestId,
181
184
  progress: Math.min(completedCount, totalCount) / totalCount,
182
185
  // Clients expect these to be strings
183
- data: JSON.stringify(responseData),
186
+ data: JSON.stringify(responseData || ''),
184
187
  info: this.tool || ''
185
188
  });
186
189
  }
@@ -121,8 +121,8 @@ class AzureVideoTranslatePlugin extends ModelPlugin {
121
121
  const operationUrl = response.headers['operation-location'];
122
122
  return { translation: response.data, operationUrl };
123
123
  } catch (error) {
124
- const errorText = error.response?.data || error.message;
125
- throw new Error(`Failed to create translation: ${error.message}\nDetails: ${errorText}`);
124
+ const errorText = error.response?.data?.error?.innererror?.message || error.message;
125
+ throw new Error(`Failed to create translation: ${errorText}`);
126
126
  }
127
127
  }
128
128
 
@@ -151,8 +151,8 @@ class AzureVideoTranslatePlugin extends ModelPlugin {
151
151
  });
152
152
  return response.data;
153
153
  } catch (error) {
154
- const errorText = error.response?.data || error.message;
155
- throw new Error(`Failed to get iteration status: ${error.message}\nDetails: ${errorText}`);
154
+ const errorText = error.response?.data?.error?.innererror?.message || error.message;
155
+ throw new Error(`Failed to get iteration status: ${errorText}`);
156
156
  }
157
157
  }
158
158
 
@@ -165,8 +165,8 @@ class AzureVideoTranslatePlugin extends ModelPlugin {
165
165
  });
166
166
  return response.data;
167
167
  } catch (error) {
168
- const errorText = error.response?.data || error.message;
169
- throw new Error(`Failed to poll operation: ${error.message}\nDetails: ${errorText}`);
168
+ const errorText = error.response?.data?.error?.innererror?.message || error.message;
169
+ throw new Error(`Failed to poll operation: ${errorText}`);
170
170
  }
171
171
  }
172
172
 
@@ -360,8 +360,8 @@ class AzureVideoTranslatePlugin extends ModelPlugin {
360
360
  const output = await this.getTranslationOutput(translationId, iteration.id);
361
361
  return JSON.stringify(output);
362
362
  } catch (error) {
363
- const errorText = error.response?.data || error.message;
364
- throw new Error(`Failed to create iteration: ${error.message}\nDetails: ${errorText}`);
363
+ const errorText = error.response?.data?.error?.innererror?.message || error.message;
364
+ throw new Error(`Failed to create iteration: ${errorText}`);
365
365
  }
366
366
  } catch (error) {
367
367
  logger.error(`Error in video translation: ${error.message}`);