Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/services/clients/claude/index.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// https://github.com/Explosion-Scratch/claude-unofficial-api
/* eslint-disable */

import { fetchSSE } from '../../../utils/fetch-sse'
import { fetchSSE } from '../../../utils/fetch-sse.mjs'
import { isEmpty } from 'lodash-es'

/**
Expand Down Expand Up @@ -190,7 +190,11 @@ export class Claude {
if (organizations.error) {
throw new Error(organizations.error)
}
this.organizationId = organizations[0].uuid
// Accounts linked to the Anthropic Console also list an API-only organization, which
// claude.ai denies chat access to, so prefer the organization that can chat
const organization =
organizations.find((org) => org.capabilities?.includes('chat')) || organizations[0]
this.organizationId = organization.uuid
this.recent_conversations = await this.getConversations()
this.ready = true
}
Expand Down Expand Up @@ -348,6 +352,9 @@ export class Claude {
},
)
const json = await response.json()
if (!Array.isArray(json)) {
throw new Error(json?.error?.message || JSON.stringify(json))
}
return json.map((convo) => new Conversation(this, { conversationId: convo.uuid, ...convo }))
}
/**
Expand Down
65 changes: 65 additions & 0 deletions tests/unit/services/clients/claude.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import Claude from '../../../../src/services/clients/claude/index.mjs'

const API_ORG = { uuid: 'api-org', capabilities: ['api', 'customer_terms:standard'] }
const CHAT_ORG = { uuid: 'chat-org', capabilities: ['chat', 'claude_pro'] }

function createClient(routes) {
const requests = []
const fetch = async (url) => {
requests.push(url)
const path = new URL(url).pathname
if (!(path in routes)) throw new Error('unexpected request ' + path)
return new Response(JSON.stringify(routes[path]))
}
return { client: new Claude({ sessionKey: 'sk-ant-sid02-test', fetch }), requests }
}

test('Claude init prefers the organization with chat capability', async () => {
const { client, requests } = createClient({
'/api/organizations': [API_ORG, CHAT_ORG],
'/api/organizations/chat-org/chat_conversations': [{ uuid: 'c1', name: 'First' }],
})

await client.init()

assert.equal(client.organizationId, 'chat-org')
assert.deepEqual(
client.recent_conversations.map((c) => c.conversationId),
['c1'],
)
assert.ok(requests.every((url) => !url.includes('api-org')))
})

test('Claude init falls back to the first organization when none has chat capability', async () => {
const { client } = createClient({
'/api/organizations': [{ uuid: 'first-org' }, { uuid: 'second-org', capabilities: ['api'] }],
'/api/organizations/first-org/chat_conversations': [],
})

await client.init()

assert.equal(client.organizationId, 'first-org')
})

test('Claude init reports the claude.ai error when conversations are denied', async () => {
const { client } = createClient({
'/api/organizations': [API_ORG],
'/api/organizations/api-org/chat_conversations': {
type: 'error',
error: { type: 'permission_error', message: 'Permission denied.' },
},
})

await assert.rejects(client.init(), { message: 'Permission denied.' })
})

test('Claude init reports the raw response when conversations are not a list', async () => {
const { client } = createClient({
'/api/organizations': [CHAT_ORG],
'/api/organizations/chat-org/chat_conversations': { data: [] },
})

await assert.rejects(client.init(), { message: '{"data":[]}' })
})