converse-mcp-server 3.7.1 → 4.0.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.
@@ -16,14 +16,20 @@
16
16
  import { debugLog, debugError } from '../utils/console.js';
17
17
  import { ProviderError, ErrorCodes, StopReasons } from './interface.js';
18
18
  import { clampReasoningEffort } from '../utils/reasoningEffort.js';
19
+ import { findCatalogEntry, findCatalogId } from '../utils/modelCatalog.js';
20
+ import {
21
+ hasClaudeCredentials,
22
+ isPackageResolvable,
23
+ } from '../utils/localProviderAuth.js';
19
24
 
20
- // Default underlying model when the request is just "claude" (or "claude:opus")
21
- const DEFAULT_SDK_MODEL = 'claude-opus-5-5';
25
+ const DEFAULT_MODEL = 'claude-opus-5-5';
22
26
  const SDK_EFFORT_TIERS = ['low', 'medium', 'high', 'xhigh', 'max'];
27
+ const CLAUDE_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk';
23
28
 
24
- // Supported Claude SDK models with their configurations
29
+ // Keyed by the model ID the Agent SDK accepts, which is also the canonical ID
30
+ // the router resolves to.
25
31
  const SUPPORTED_MODELS = {
26
- opus: {
32
+ 'claude-opus-5-5': {
27
33
  modelName: 'claude-opus-5-5',
28
34
  friendlyName: 'Claude Opus 5.5 (via Agent SDK)',
29
35
  contextWindow: 1000000,
@@ -35,18 +41,14 @@ const SUPPORTED_MODELS = {
35
41
  description:
36
42
  'Claude Opus 5.5 via Agent SDK (default) - requires claude login authentication',
37
43
  aliases: [
38
- 'claude',
39
- 'claude-sdk',
40
- 'claude-code',
41
- 'claude:opus',
44
+ 'opus',
42
45
  'claude-opus',
43
- 'claude-opus-5-5',
44
46
  'claude-opus-5.5',
45
47
  'opus-5-5',
46
48
  'opus-5.5',
47
49
  ],
48
50
  },
49
- 'opus-5': {
51
+ 'claude-opus-5': {
50
52
  modelName: 'claude-opus-5',
51
53
  friendlyName: 'Claude Opus 5 (via Agent SDK)',
52
54
  contextWindow: 1000000,
@@ -57,9 +59,9 @@ const SUPPORTED_MODELS = {
57
59
  timeout: 1800000,
58
60
  description:
59
61
  'Claude Opus 5 via Agent SDK - requires claude login authentication',
60
- aliases: ['claude-opus-5'],
62
+ aliases: ['opus-5', 'opus5'],
61
63
  },
62
- fable: {
64
+ 'claude-fable-5-1': {
63
65
  modelName: 'claude-fable-5-1',
64
66
  friendlyName: 'Claude Fable 5.1 (via Agent SDK)',
65
67
  contextWindow: 1000000,
@@ -71,15 +73,14 @@ const SUPPORTED_MODELS = {
71
73
  description:
72
74
  'Claude Fable 5.1 via Agent SDK - requires claude login authentication',
73
75
  aliases: [
74
- 'claude:fable',
76
+ 'fable',
75
77
  'claude-fable',
76
- 'claude-fable-5-1',
77
78
  'claude-fable-5.1',
78
79
  'fable-5-1',
79
80
  'fable-5.1',
80
81
  ],
81
82
  },
82
- 'fable-5': {
83
+ 'claude-fable-5': {
83
84
  modelName: 'claude-fable-5',
84
85
  friendlyName: 'Claude Fable 5 (via Agent SDK)',
85
86
  contextWindow: 1000000,
@@ -90,7 +91,7 @@ const SUPPORTED_MODELS = {
90
91
  timeout: 1800000,
91
92
  description:
92
93
  'Claude Fable 5 via Agent SDK - requires claude login authentication',
93
- aliases: ['claude-fable-5'],
94
+ aliases: ['fable-5', 'fable5'],
94
95
  },
95
96
  };
96
97
 
@@ -104,25 +105,12 @@ class ClaudeProviderError extends ProviderError {
104
105
  }
105
106
  }
106
107
 
107
- /**
108
- * Check if Claude SDK is available (optional dependency)
109
- */
110
- function isClaudeSDKAvailable() {
111
- try {
112
- // Simple presence check that works in ES modules
113
- // If SDK not available, the actual import() will fail later with clear error
114
- return true;
115
- } catch {
116
- return false;
117
- }
118
- }
119
-
120
108
  /**
121
109
  * Dynamically import Claude SDK (lazy loading)
122
110
  * This keeps the SDK as an optional dependency
123
111
  */
124
112
  async function getClaudeSDK() {
125
- if (!isClaudeSDKAvailable()) {
113
+ if (!isPackageResolvable(CLAUDE_SDK_PACKAGE)) {
126
114
  throw new ClaudeProviderError(
127
115
  'Claude SDK not installed. Install with: npm install @anthropic-ai/claude-agent-sdk',
128
116
  'CLAUDE_SDK_NOT_INSTALLED',
@@ -143,58 +131,24 @@ async function getClaudeSDK() {
143
131
  }
144
132
 
145
133
  /**
146
- * Look up model config from SUPPORTED_MODELS by name or alias.
147
- * Strips the claude: prefix first (e.g. "claude:opus" -> "opus").
148
- */
149
- function findModelConfig(modelName) {
150
- if (typeof modelName !== 'string') return null;
151
-
152
- let name = modelName.trim();
153
- if (name.toLowerCase().startsWith('claude:')) {
154
- name = name.slice('claude:'.length).trim();
155
- }
156
- if (!name) return SUPPORTED_MODELS.opus;
157
-
158
- const nameLower = name.toLowerCase();
159
-
160
- if (SUPPORTED_MODELS[nameLower]) {
161
- return SUPPORTED_MODELS[nameLower];
162
- }
163
-
164
- for (const config of Object.values(SUPPORTED_MODELS)) {
165
- if (
166
- config.aliases &&
167
- config.aliases.some((alias) => alias.toLowerCase() === nameLower)
168
- ) {
169
- return config;
170
- }
171
- }
172
-
173
- return null;
174
- }
175
-
176
- /**
177
- * Resolve the requested model to the underlying SDK model ID.
178
- * - "claude" (and bare "claude:") defaults to Claude Opus 5.5
179
- * - "claude:opus" / "claude:opus-5" / "claude:fable" / "claude:fable-5" select the specific model
180
- * - Unknown names are passed through (after prefix stripping) so users can
181
- * target any model ID the Agent SDK accepts (e.g. "claude:claude-sonnet-4-6")
134
+ * Resolve a model name to the Agent SDK model ID. The router hands over
135
+ * canonical catalog IDs; aliases are accepted for direct callers.
136
+ * @param {string} [modelName] - Catalog ID or alias; defaults to DEFAULT_MODEL
137
+ * @returns {string}
138
+ * @throws {ClaudeProviderError} When the name is not in the catalog
182
139
  */
183
140
  function resolveSdkModel(modelName) {
184
141
  if (typeof modelName !== 'string' || !modelName.trim()) {
185
- return DEFAULT_SDK_MODEL;
142
+ return DEFAULT_MODEL;
186
143
  }
187
-
188
- const config = findModelConfig(modelName);
189
- if (config) {
190
- return config.modelName;
191
- }
192
-
193
- let name = modelName.trim();
194
- if (name.toLowerCase().startsWith('claude:')) {
195
- name = name.slice('claude:'.length).trim();
144
+ const id = findCatalogId(SUPPORTED_MODELS, modelName);
145
+ if (!id) {
146
+ throw new ClaudeProviderError(
147
+ `Unknown Claude model "${modelName}"`,
148
+ ErrorCodes.MODEL_NOT_FOUND,
149
+ );
196
150
  }
197
- return name || DEFAULT_SDK_MODEL;
151
+ return SUPPORTED_MODELS[id].modelName;
198
152
  }
199
153
 
200
154
  /**
@@ -338,7 +292,7 @@ async function* createStreamingGenerator(
338
292
  try {
339
293
  const queryOptions = {
340
294
  ...options,
341
- model: options.model || DEFAULT_SDK_MODEL,
295
+ model: options.model || DEFAULT_MODEL,
342
296
  maxTurns: 100,
343
297
  permissionMode: 'bypassPermissions', // Don't prompt for permissions
344
298
  };
@@ -475,7 +429,7 @@ export const claudeProvider = {
475
429
  */
476
430
  async invoke(messages, options = {}) {
477
431
  const {
478
- model = 'claude',
432
+ model = DEFAULT_MODEL,
479
433
  config,
480
434
  stream = false,
481
435
  signal,
@@ -616,16 +570,14 @@ export const claudeProvider = {
616
570
  }
617
571
  },
618
572
 
573
+ defaultModel: DEFAULT_MODEL,
574
+
619
575
  /**
620
- * Validate Claude SDK configuration
621
- * Claude SDK uses CLI authentication (NOT API keys)
622
- * Returns true optimistically - authentication errors handled at runtime
576
+ * Validate Claude SDK configuration: the SDK is installed and a Claude Code
577
+ * credential is present. Expired logins surface at invoke time.
623
578
  */
624
579
  validateConfig(_config) {
625
- // Claude SDK uses CLI authentication, not API keys
626
- // We can't reliably check auth status, so return true optimistically
627
- // and let the SDK handle authentication errors during execution
628
- return isClaudeSDKAvailable();
580
+ return isPackageResolvable(CLAUDE_SDK_PACKAGE) && hasClaudeCredentials();
629
581
  },
630
582
 
631
583
  /**
@@ -644,9 +596,8 @@ export const claudeProvider = {
644
596
 
645
597
  /**
646
598
  * Get model configuration for specific model
647
- * Handles claude: prefixed names (e.g. "claude:opus", "claude:fable", "claude:opus-5")
648
599
  */
649
600
  getModelConfig(modelName) {
650
- return findModelConfig(modelName);
601
+ return findCatalogEntry(SUPPORTED_MODELS, modelName);
651
602
  },
652
603
  };
@@ -20,10 +20,15 @@ import {
20
20
  EFFORT_LADDER,
21
21
  clampReasoningEffort,
22
22
  } from '../utils/reasoningEffort.js';
23
+ import { findCatalogEntry, findCatalogId } from '../utils/modelCatalog.js';
24
+ import {
25
+ hasCodexCredentials,
26
+ isPackageResolvable,
27
+ } from '../utils/localProviderAuth.js';
23
28
 
24
29
  /**
25
- * Backend models Codex can run, keyed by the slug passed to the CLI as
26
- * --model. The reasoning tiers are the ones each model's API accepts, verified
30
+ * Models Codex can run, keyed by the slug passed to the CLI as --model. The
31
+ * catalog key is the canonical model ID the router resolves to. The reasoning tiers are the ones each model's API accepts, verified
27
32
  * against the API's own rejection messages (gpt-6-astra: "Supported values
28
33
  * are: 'low', 'medium', 'high', 'xhigh', and 'max'"; the Sol/Luna tiers of
29
34
  * both generations accept 'none' as well). The SDK's ModelReasoningEffort
@@ -38,68 +43,56 @@ import {
38
43
  * it reasons — so it is deliberately absent from every supportedEfforts list
39
44
  * and nothing at the tool level can select it.
40
45
  */
41
- const CODEX_BACKEND_MODELS = {
42
- 'gpt-6-sol': {
46
+ const ALL_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
47
+
48
+ function codexModel(slug, friendlyName, { aliases, contextWindow = 272000, supportedEfforts = ALL_EFFORTS }) {
49
+ return {
50
+ modelName: slug,
51
+ friendlyName: `OpenAI Codex (${friendlyName})`,
52
+ contextWindow,
53
+ maxOutputTokens: 128000,
54
+ supportsStreaming: true,
55
+ supportsImages: true, // Codex SDK 0.118+ supports images via --image (local_image input)
56
+ supportsWebSearch: false, // Codex accesses files directly, not web
57
+ timeout: 1800000, // 30 minutes
58
+ description: `${friendlyName} via the Codex agentic coding assistant, with local file access and tool execution`,
59
+ aliases,
60
+ supportedEfforts,
61
+ };
62
+ }
63
+
64
+ const SUPPORTED_MODELS = {
65
+ 'gpt-6-sol': codexModel('gpt-6-sol', 'GPT-6 Sol', {
43
66
  aliases: ['sol', 'gpt-6', 'gpt6', 'gpt6-sol', 'gpt-6-codex'],
44
- contextWindow: 272000,
45
- supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
46
- },
47
- 'gpt-6-luna': {
67
+ }),
68
+ 'gpt-6-luna': codexModel('gpt-6-luna', 'GPT-6 Luna', {
48
69
  aliases: ['luna', 'gpt6-luna'],
49
- contextWindow: 272000,
50
- supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
51
- },
52
- 'gpt-6-astra': {
70
+ }),
71
+ 'gpt-6-astra': codexModel('gpt-6-astra', 'GPT-6 Astra', {
53
72
  aliases: ['astra', 'gpt6-astra'],
54
- contextWindow: 272000,
55
73
  supportedEfforts: ['low', 'medium', 'high', 'xhigh', 'max'],
56
- },
57
- 'gpt-5.6-sol': {
74
+ }),
75
+ 'gpt-5.6-sol': codexModel('gpt-5.6-sol', 'GPT-5.6 Sol', {
58
76
  aliases: ['gpt-5.6', 'gpt5.6', 'gpt5.6-sol', 'gpt-5.6-codex'],
59
- contextWindow: 272000,
60
- supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
61
- },
62
- 'gpt-5.6-terra': {
77
+ }),
78
+ 'gpt-5.6-terra': codexModel('gpt-5.6-terra', 'GPT-5.6 Terra', {
63
79
  aliases: ['terra', 'gpt5.6-terra'],
64
- contextWindow: 272000,
65
- supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
66
- },
67
- 'gpt-5.6-luna': {
80
+ }),
81
+ 'gpt-5.6-luna': codexModel('gpt-5.6-luna', 'GPT-5.6 Luna', {
68
82
  aliases: ['gpt5.6-luna'],
69
- contextWindow: 272000,
70
- supportedEfforts: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
71
- },
72
- 'gpt-5.5': {
83
+ }),
84
+ 'gpt-5.5': codexModel('gpt-5.5', 'GPT-5.5', {
73
85
  aliases: ['gpt5.5'],
74
- contextWindow: 272000,
75
86
  supportedEfforts: ['low', 'medium', 'high', 'xhigh'],
76
- },
77
- 'gpt-5.3-codex-spark': {
87
+ }),
88
+ 'gpt-5.3-codex-spark': codexModel('gpt-5.3-codex-spark', 'GPT-5.3 Codex Spark', {
78
89
  aliases: ['spark', 'codex-spark'],
79
90
  contextWindow: 128000,
80
91
  supportedEfforts: ['low', 'medium', 'high', 'xhigh'],
81
- },
92
+ }),
82
93
  };
83
94
 
84
- const DEFAULT_BACKEND_MODEL = 'gpt-6-sol';
85
-
86
- // The single user-facing model the router exposes. The backend model behind it
87
- // comes from CODEX_MODEL, or from a `codex:<model>` spec.
88
- const SUPPORTED_MODELS = {
89
- codex: {
90
- modelName: 'codex',
91
- friendlyName: 'OpenAI Codex (GPT-6 Sol)',
92
- contextWindow: CODEX_BACKEND_MODELS[DEFAULT_BACKEND_MODEL].contextWindow,
93
- maxOutputTokens: 128000,
94
- supportsStreaming: true,
95
- supportsImages: true, // Codex SDK 0.118+ supports images via --image (local_image input)
96
- supportsWebSearch: false, // Codex accesses files directly, not web
97
- timeout: 1800000, // 30 minutes
98
- description:
99
- 'OpenAI Codex agentic coding assistant with local file access and tool execution (GPT-6 Sol by default; pick another backend with codex:<model> or CODEX_MODEL)',
100
- aliases: [],
101
- },
102
- };
95
+ const DEFAULT_MODEL = 'gpt-6-sol';
103
96
 
104
97
  /**
105
98
  * Custom error class for Codex provider errors
@@ -111,26 +104,14 @@ class CodexProviderError extends ProviderError {
111
104
  }
112
105
  }
113
106
 
114
- /**
115
- * Check if Codex SDK is available (optional dependency)
116
- * Uses import.meta.resolve when available, falls back to filesystem check
117
- */
118
- function isCodexAvailable() {
119
- try {
120
- // Just try to dynamically check if we can import it
121
- // This is a simple presence check that works in ES modules
122
- return true; // If SDK not available, the actual import() will fail later with clear error
123
- } catch {
124
- return false;
125
- }
126
- }
107
+ const CODEX_SDK_PACKAGE = '@openai/codex-sdk';
127
108
 
128
109
  /**
129
110
  * Dynamically import Codex SDK (lazy loading)
130
111
  * This keeps the SDK as an optional dependency
131
112
  */
132
113
  async function getCodexSDK() {
133
- if (!isCodexAvailable()) {
114
+ if (!isPackageResolvable(CODEX_SDK_PACKAGE)) {
134
115
  throw new CodexProviderError(
135
116
  'Codex SDK not installed. Install with: npm install @openai/codex-sdk',
136
117
  'CODEX_NOT_INSTALLED',
@@ -248,54 +229,24 @@ function extractPromptText(input) {
248
229
  }
249
230
 
250
231
  /**
251
- * Resolve a backend slug or alias (case-insensitive) to its catalog slug.
252
- * @param {string} name
253
- * @returns {string|null}
232
+ * Resolve a model name to the slug passed to the CLI. The router hands over
233
+ * canonical catalog IDs; aliases are accepted for direct callers.
234
+ * @param {string} [model] - Catalog ID or alias; defaults to DEFAULT_MODEL
235
+ * @returns {string} Backend slug for the SDK's `model` option
236
+ * @throws {CodexProviderError} When the name is not in the catalog
254
237
  */
255
- function findBackendSlug(name) {
256
- const lower = String(name || '').trim().toLowerCase();
257
- if (!lower) {
258
- return null;
238
+ export function resolveBackendModel(model) {
239
+ if (model === undefined || model === null || model === '') {
240
+ return DEFAULT_MODEL;
259
241
  }
260
- if (CODEX_BACKEND_MODELS[lower]) {
261
- return lower;
242
+ const slug = findCatalogId(SUPPORTED_MODELS, model);
243
+ if (!slug) {
244
+ throw new CodexProviderError(
245
+ `Unknown Codex model "${model}"`,
246
+ ErrorCodes.MODEL_NOT_FOUND,
247
+ );
262
248
  }
263
- return (
264
- Object.keys(CODEX_BACKEND_MODELS).find((slug) =>
265
- CODEX_BACKEND_MODELS[slug].aliases.includes(lower),
266
- ) || null
267
- );
268
- }
269
-
270
- /**
271
- * Catalog entry for a backend model, or null when the slug is not catalogued.
272
- * @param {string} name - Backend slug or alias
273
- * @returns {{ slug: string, aliases: string[], contextWindow: number, supportedEfforts: string[] }|null}
274
- */
275
- export function getBackendModelConfig(name) {
276
- const slug = findBackendSlug(name);
277
- return slug ? { slug, ...CODEX_BACKEND_MODELS[slug] } : null;
278
- }
279
-
280
- /**
281
- * Resolve the requested model spec to the backend slug passed to the CLI.
282
- *
283
- * `codex` uses CODEX_MODEL (default gpt-6-sol); `codex:<model>` names a
284
- * backend directly, by slug or alias. Unknown names pass through verbatim so a
285
- * newly released model works before it is catalogued here — the CLI rejects
286
- * anything the backend does not know.
287
- *
288
- * @param {string} spec - Requested model, e.g. 'codex' or 'codex:sol'
289
- * @param {Object} [config] - Loaded configuration
290
- * @returns {string} Backend slug for the SDK's `model` option
291
- */
292
- export function resolveBackendModel(spec, config) {
293
- const raw = String(spec || '').trim();
294
- const requested = raw.toLowerCase().startsWith('codex:')
295
- ? raw.slice('codex:'.length).trim()
296
- : '';
297
- const name = requested || config?.providers?.codexmodel || DEFAULT_BACKEND_MODEL;
298
- return findBackendSlug(name) || name;
249
+ return slug;
299
250
  }
300
251
 
301
252
  /**
@@ -319,30 +270,6 @@ async function getThreadIdFromContinuation(
319
270
  }
320
271
  }
321
272
 
322
- /**
323
- * Resolve a user-facing model name to its entry in SUPPORTED_MODELS.
324
- * @param {string} modelName
325
- * @returns {Object|null}
326
- */
327
- function findModelConfig(modelName) {
328
- const modelNameLower = String(modelName || '').toLowerCase();
329
-
330
- if (SUPPORTED_MODELS[modelNameLower]) {
331
- return SUPPORTED_MODELS[modelNameLower];
332
- }
333
-
334
- for (const config of Object.values(SUPPORTED_MODELS)) {
335
- if (
336
- config.aliases &&
337
- config.aliases.some((alias) => alias.toLowerCase() === modelNameLower)
338
- ) {
339
- return config;
340
- }
341
- }
342
-
343
- return null;
344
- }
345
-
346
273
  /**
347
274
  * Create stream generator for Codex streaming responses.
348
275
  * `input` is the Codex SDK Input (string | UserInput[]) — strings for plain
@@ -382,7 +309,7 @@ export const codexProvider = {
382
309
  */
383
310
  async invoke(messages, options = {}) {
384
311
  const {
385
- model = 'codex',
312
+ model = DEFAULT_MODEL,
386
313
  config,
387
314
  stream = false,
388
315
  signal,
@@ -400,6 +327,10 @@ export const codexProvider = {
400
327
  );
401
328
  }
402
329
 
330
+ // Resolved outside the try so an unknown model keeps MODEL_NOT_FOUND
331
+ // instead of being rewrapped as a generic execution failure.
332
+ const backendModel = resolveBackendModel(model);
333
+
403
334
  try {
404
335
  // Get Codex SDK
405
336
  const Codex = await getCodexSDK();
@@ -456,7 +387,6 @@ export const codexProvider = {
456
387
  const approvalPolicy = config.providers?.codexapprovalpolicy || 'never';
457
388
 
458
389
  // Create or resume thread
459
- const backendModel = resolveBackendModel(model, config);
460
390
  const threadOptions = {
461
391
  model: backendModel,
462
392
  workingDirectory,
@@ -467,7 +397,7 @@ export const codexProvider = {
467
397
 
468
398
  if (reasoning_effort) {
469
399
  const supportedEfforts =
470
- getBackendModelConfig(backendModel)?.supportedEfforts || EFFORT_LADDER;
400
+ SUPPORTED_MODELS[backendModel]?.supportedEfforts || EFFORT_LADDER;
471
401
  const mappedEffort = clampReasoningEffort(reasoning_effort, supportedEfforts);
472
402
  threadOptions.modelReasoningEffort = mappedEffort;
473
403
  if (mappedEffort !== reasoning_effort) {
@@ -575,15 +505,14 @@ export const codexProvider = {
575
505
  }
576
506
  },
577
507
 
508
+ defaultModel: DEFAULT_MODEL,
509
+
578
510
  /**
579
- * Validate Codex configuration
580
- * Codex uses ChatGPT authentication or CODEX_API_KEY (NOT OPENAI_API_KEY)
511
+ * Validate Codex configuration: the SDK is installed and a credential is
512
+ * present (ChatGPT login file or CODEX_API_KEY — NOT OPENAI_API_KEY).
581
513
  */
582
- validateConfig(_config) {
583
- // Codex can work with either ChatGPT login or API key
584
- // Since we can't reliably check ChatGPT login status, we'll be permissive
585
- // and let the SDK handle authentication errors
586
- return isCodexAvailable();
514
+ validateConfig(config) {
515
+ return isPackageResolvable(CODEX_SDK_PACKAGE) && hasCodexCredentials(config);
587
516
  },
588
517
 
589
518
  /**
@@ -604,6 +533,6 @@ export const codexProvider = {
604
533
  * Get model configuration for specific model
605
534
  */
606
535
  getModelConfig(modelName) {
607
- return findModelConfig(modelName);
536
+ return findCatalogEntry(SUPPORTED_MODELS, modelName);
608
537
  },
609
538
  };